Using Google Sheets as a Wait List Form with Next.js and Resend.

Pic thumbnail

When making any form in a very lean way, we can use google sheets as a way of storing all that data. This may not be the best solution when it comes to handling user data or data you are repetively fetching, there you would want to use a postgreSQL database. But lets say you are storing data that only gets pushed to and never read to like a waitlist or inquiry form, then using google sheets might be the solution for you then follow up with sending an email with the resend api.

If you want to just see the code heres a repo for all about it: https://github.com/lorenzejay/nextjs-gsheets-resend

When To Use Google Sheets

  1. Only making write requests like a google sheet

Don't use it as a way to store user data

Instead ompt for traditional databases like postgreSQL or mongoDB.

Here's a step by step guide:

1. Scaffold a next.js app

npx create-next-app@latest my-app --typescript --tailwind --eslint


Also install npm i googleapis resend

2. Create a actions.ts file in your /src directory

'use server'
import { google } from 'googleapis'
import { revalidatePath } from 'next/cache'
import { Resend } from 'resend'

function getCurrentDateTime(): string {
  const now = new Date()
  const year = now.getFullYear()
  const month = (now.getMonth() + 1).toString().padStart(2, '0') // Months are 0-based in JavaScript, so add 1
  const day = now.getDate().toString().padStart(2, '0')
  const hours = now.getHours().toString().padStart(2, '0')
  const minutes = now.getMinutes().toString().padStart(2, '0')
  const seconds = now.getSeconds().toString().padStart(2, '0')

  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

export async function addToContactListAction(_: any, formData: FormData) {
  const Name = formData.get('name')
  const Email = formData.get('email')
  const Company = formData.get('company')
  const Mobile_Phone = formData.get('phone')
  const Message = formData.get('message')

  const Signup_Date = getCurrentDateTime()
  if (!Email || !Name || !Company || !Mobile_Phone || !Signup_Date)
    throw new Error('Missing form entries')

  const private_key = process.env.GOOGLE_PRIVATE_KEY?.replace(/['"]/g, '')

  if (!private_key) throw new Error('no key')
  try {
    const auth = await google.auth.getClient({
      projectId: process.env.GOOGLE_PROJECT_ID,
      credentials: {
        type: 'service_account',
        private_key: private_key?.replace(/\\n/g, '\n'),
        client_email: process.env.GOOGLE_CLIENT_EMAIL,
        client_id: process.env.GOOGLE_CLIENT_ID,
        token_url: process.env.GOOGLE_TOKEN_URL,
        universe_domain: 'googleapis.com',
      },
      scopes: ['https://www.googleapis.com/auth/spreadsheets'],
      // keyFile: 'path-to-your-service-account-key.json', // Replace with the path to your service account key file
      // scopes: 'https://www.googleapis.com/auth/spreadsheets',
    })

    const sheets = google.sheets({ version: 'v4', auth })
    const spreadsheetId = process.env.GOOGLE_SHEETS_ID
    // const { data } = req.body
    await sheets.spreadsheets.values.append({
      spreadsheetId,
      range: '[your sheet]',
      valueInputOption: 'USER_ENTERED',
      requestBody: {
        values: [[Email, Name, Company, Mobile_Phone, Message, Signup_Date]], // Data must be an array of arrays (each inner array represents a row)
      },
    })
    const resend = new Resend(process.env.RESEND_API_KEY)
    await resend.emails.send({
      from: 'hello@yourdomain.com',
      to: Email as string,
      subject: 'We recieved your contact and will get to you within 24 Hours.',
      html: `
      <p>Hi, ${Name}.</p>
      <p>Thank you for reaching out to us. We appreciate your inquiry and value the opportunity to connect with you.</p>
      <p>We’ve received your message and will get back to you within 24 hours.</p>
      <br/>
      <p>Best Regards,</p>
      <p>Lorenze</p>
      `,
    })
    // then notiffy me
    await resend.emails.send({
      from: 'alert@yourdomain.com',
      to: 'lorenze@yourdomain.com',
      subject: `New Contact Inquiry from ${Name}`,
      html: `
      Details:
      <b>Name:</b> ${Name}
      <br />
      <b>Email:</b> ${Email}
      <br />
      <b>Company:</b> ${Company}
      <br />
      <b>Phone Number:</b> ${Mobile_Phone}
      <br />
      <b>Message:</b> ${Message}
      `,
    })

    revalidatePath('/')
    return { message: 'Successfully signed up' }
  } catch (error: any) {
    // errors could be from resend or from googleapis
    console.log('contact submit error:', error)

    return { message: 'Failed contact' }
  }
}

Use your server action in a client component

'use client'

import { useId, useRef, useState } from 'react'
import { addToContactListAction } from '@/app/actions'
import FormSubmitButton from './FormSubmitButton'

const initialState = {
  message: null,
}

const ContactForm = () => {
  const ref = useRef<HTMLFormElement>(null)

  const [state, setState] = useState<{ message: string } | null>()
  return (
    <div className="lg:order-last">
      <form
        ref={ref}
        action={async (formData) => {
          const res = await addToContactListAction(initialState, formData)
          setState(res)
          ref?.current?.reset()
        }}
      >
        <h2 className="font-display text-base font-semibold text-neutral-950">
          Work inquiries
        </h2>
        <div className="isolate mt-6 -space-y-px rounded-2xl bg-white/50">
          <TextInput label="Name" name="name" autoComplete="name" />
          <TextInput
            label="Email"
            type="email"
            name="email"
            autoComplete="email"
          />
          <TextInput
            label="Company"
            name="company"
            autoComplete="organization"
          />
          <TextInput label="Phone" type="tel" name="phone" autoComplete="tel" />
          <TextInput label="Message" name="message" />
        </div>
        <FormSubmitButton />
      </form>
      {state?.message !== null && (
        <p className="mt-4 text-center text-green-500">{state?.message}</p>
      )}
    </div>
  )
}

export default ContactForm

Should look like this:

demo gif

And if its successful you should have a new row on your sheet.

demo pic

And on resend it should send an email to the email put on resend pic

Conclusion

You can use a 2 awesome tools to make a free waitlist service for your products landing page with next.js. Especially with Next.js server actions you can integrate resend and googleapis easily and securely running your calls on the server.

If you want to clone a repo, you can do so here: https://github.com/lorenzejay/nextjs-gsheets-resend