DEV Community

Temi Kara
Temi Kara

Posted on

Build a Serverless Contact Form with Firebase, Firestore, and Cloud Functions in Next.js

serverless contact form

Building a backend application to handle a simple contact form on your webpage might be an overkill for someone who only wants to send an acknowledgement email to the user/customer who contacted them and also receive an email that they were contacted by a user/customer. A good way to work around this is to use firebase and cloud function.

Firebase is Google's cloud storage platform with a lot of added functionalities, in this post I will be using firestore to store contact form information and then trigger a cloud function whenever a new information is created in firestore.

Table of Contents

signup on firebase
create a project
frontend integration with SDK
creating cloud function

Signup on Firebase

This step is pretty straight forward, all you need to do is visit https://console.firebase.google.com with your Gmail account. when you're done, you should see a welcome screen like this.

Firebase Signup

Create a Project

After signing up, go ahead to create a project by clicking Get started by setting up a firebase project. You'll se an option to enter your project ID, I'll name mine tutorial-contact-form then check the accept terms and conditions box and click continue.

create firebase project

Keep clicking continue till you get to configure google analytics, select your location in the dropdown menu, accept the terms and conditions then click continue
google analytics

After this step your project is created on the platform. Click continue to view your project, you should see a screen like this when you are done with these steps

firebase project

Select the project you just created then click add app. You'll see a couple of options, select web and give the app a name (I'll name mine contact form) then click register app. you should see your SDK information when app registration is done. Scroll down and click continue to console

firebase web app SDK

Frontend Integration With SDK

Now that we have our SDK, we can go ahead to use it in our frontend application form. My frontend app is a next.js application with a simple contact form having three fields which are name, email, and message fields.

contact form

But before we do that, we need to create a firestore database. To do this, select firestore form databases & storage option on the sidebar in your firebase app and click on create database, you should see a screen like this.

create firestore database

Select the standard option, click next, you can leave the ID field blank so firestore generates an ID for you, select a location closest to you to reduce latency and click next. Select production mode and click create. Firestore provisions a database for you in few seconds and you should see a screen like this

create firestore database

Next thing we want to do is create a collection by clicking on start collection, a modal pops up for you to fill in the collection ID I'm calling mine enquiry, click next to create the first document of the collection with the fields matching our contact form mine has name, email, and message.

For document ID we can click auto ID then start adding document fields. First field I'll add is the name field, the type will be string then enter a name in the input field below, click on add field and repeat this for the remaining fields and click save when you're done. You should see the document you just created

document creation on firestore

Next thing we need to do is update our rules to enable document creation when certain criteria's are met, switch to the rules tab and add this code then click publish, that's if we have the same form fields, if not adjust rule to suit your contact form fields.

rules_version = '2';

service cloud.firestore {
  match /databases/(default)/documents {

    match /enquiry/{enquiryId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'email', 'message'])
        && request.resource.data.name is string
        && request.resource.data.name.size() > 0
        && request.resource.data.name.size() <= 100
        && request.resource.data.email is string
        && request.resource.data.email.size() > 0
        && request.resource.data.email.size() <= 100
        && request.resource.data.email.matches('.*@.*\\..*')
        && request.resource.data.message is string
        && request.resource.data.message.size() > 0
        && request.resource.data.message.size() <= 2000
        && request.resource.data.keys().size() == 3;

      allow read, update, delete: if false;
    }

    match /{document=**} {
      allow read, write: if false;
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

Ensure that the input size restrictions are also enforced on the frontend input fields, name and email inputs should be greater than zero and less than or equal to 100, while message should be greater than zero and less than or equal to 2000.

Now to connect to firestore, we need to install firebase in our frontend app by running npm install firebase in the console of our frontend app. Next thing is to create a .env and .env.local file at the root of our frontend app so we can safely use our SDK credentials without exposing them to the public.

Now add SDK credentials to both env files using the format below.

  NEXT_PUBLIC_FIREBASE_API_KEY = "ygefwbowerfbwjebvfwyefb"
  NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN = "jhsdaygfiuy.firebaseapp.com"
  NEXT_PUBLIC_FIREBASE_PROJECT_ID= "jhsdaygfiuy"
  NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET = "jhsdaygfiuy.firebasestorage.app"
  NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID = "7263487592847"
  NEXT_PUBLIC_FIREBASE_APP_ID = "1:7623485273469"
  NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID = "G-K7JHJEJHB"
Enter fullscreen mode Exit fullscreen mode

N.B: the values above are random values, make sure you input the correct SDK values from the firebase app that was created earlier.

Now lets create a utils folder in our frontend app's src directory and then create a file named firebase.ts in the utils folder. In the firebase.ts file, add the following code

import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app =
  getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];

export const db = getFirestore(app);
Enter fullscreen mode Exit fullscreen mode

The above code configures and initializes firestore. Next thing we want to do is create a firebase hook. To do this we create a hooks folder in our src folder and create a file called useFirestore.ts in the hooks folder, now place the following code in useFirestore.ts file

import { useState } from "react";
import { db } from "@/utils/firebase";
import {
  collection,
  addDoc,
  doc,
  setDoc,
  DocumentData,
  FirestoreError,
} from "firebase/firestore";

interface UseCreateDocReturn<T> {
  createDoc: (data: T, customId?: string | null) => Promise<string | null>;
  loading: boolean;
  error: string | null;
  success: boolean;
}

export const useFirestore = <T extends DocumentData>(
  collectionName: string,
): UseCreateDocReturn<T> => {
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState<boolean>(false);

  const createDoc = async (
    data: T,
    customId: string | null = null,
  ): Promise<string | null> => {
    setLoading(true);
    setError(null);
    setSuccess(false);

    try {
      let docRef;

      if (customId) {
        // Option A: Write data to a specific, user-defined document ID
        docRef = doc(db, collectionName, customId);
        await setDoc(docRef, data);
      } else {
        // Option B: Add data and let Firestore auto-generate the ID
        const colRef = collection(db, collectionName);
        docRef = await addDoc(colRef, data);
      }

      setSuccess(true);
      setLoading(false);
      return docRef.id; // Returns the generated or manually set ID
    } catch (err) {
      const firestoreError = err as FirestoreError;
      setError(firestoreError.message || "Something went wrong.");
      setLoading(false);
      return null;
    }
  };

  return { createDoc, loading, error, success };
};

Enter fullscreen mode Exit fullscreen mode

firebase configuration file

Now that our firestore hook is ready, we can use it in our form page by importing it into the page like this import { useFirestore } from "@/hooks/useFirestore";, in our contact page function deconstruct useFirestore like this const { createDoc, loading } = useFirestore("enquiry");. We can then pass our formData to createDoc like this const docId = await createDoc(formData); and await a response from firestore. If document creation is successful, we clear form inputs and show the user a success message, if document creation faild we show the user an error message.

firebase hook

Creating Cloud Function

At this point, we should be able to save form detail in our firestore. The next thing for us to do is to create a cloud function that gets triggered every time a new enquiry document is created in firestore. To do that, we first need to upgrade our account from spark plan to blaze plan (pay as you go plan), click on project overview on the sidebar, you should see spark plan beside your project name, click on it and select blaze plane.

google cloud plans

You will then be asked to create a billing account, not to worry, firebase has a pretty generous free limit of 2 million function calls per month before pay as you go billing starts. Click on create a cloud billing account and input all relevant details, also make sure you set a convenient monthly budget cap.

google cloud billing account

N.B: as at the time of writing this, I wasn't able to create a billing account on the tutorial contact form project, so I had to switch to one of my account that has a billing account.

If all went well with creating a billing account, next thing is to install firebase CLI tools, and we do that by running npm install -g firebase tools in our terminal. After that, we login to firebase by running firebase login and login with your google account.

Next thing is to create a folder at the root of our frontend project and name it cloudFunctions. From your terminal, change directory into cloudFunctions folder by running cd cloudFunctions, then run firebase init functions, select use an existing project since we already have a project and select your contact form project, select typescript as the language of choice and opt out of using eslint, then select yes to install dependencies. After this process, we should see some files and folders created by firebase in our cloudFunctions folder.

Navigate to the functions folder created by firebase in our cloudFunctions folder and open the src folder, we will see an index.ts file, this is where we will be writing our cloud function.

Before we write our cloud function, we need to install nodemailer by running npm install nodemailer and @types/nodemailer by running npm i --save-dev @types/nodemailer. Copy the code below and paste it in index.ts file. Make sure you update the to field in mailOptions replace admin@yourdomain.com with the email you want to send the information to.

import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { logger } from "firebase-functions";
import * as nodemailer from "nodemailer";

interface SubmissionData {
  name: string;
  email: string;
  message: string;
}

// Configure your SMTP or Email Provider transporter
const transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
});

export const onNewSubmission = onDocumentCreated(
  "submissions/{docId}",
  async (event) => {
    const snapshot = event.data;
    if (!snapshot) return null;

    const data = snapshot.data() as SubmissionData;

    const mailOptions: nodemailer.SendMailOptions = {
      from: "Your Website Contact Form <no-reply@yourdomain.com>",
      to: "admin@yourdomain.com", // Where you want to receive alerts
      subject: `New Contact Form Submission from ${data.name}`,
      html: `
        <p><strong>Name:</strong> ${data.name}</p>
        <p><strong>Email:</strong> ${data.email}</p>
        <p><strong>Message:</strong> ${data.message}</p>
      `,
    };

    try {
      await transporter.sendMail(mailOptions);
      logger.log(`Notification email sent for document ${event.params.docId}`);
    } catch (error) {
      logger.error("Failed to send email:", error);
    }

    return null;
  },
);

Enter fullscreen mode Exit fullscreen mode

file structure image

N.B: You can replace the html template with your preferred template and you can also send a message to the person making an enquiry by creating a second mailOptions and setting the to field to data.email.

Notice we have a section in the code where we need to authenticate our account to use nodemailer, for this we need to create an app password as your regular Gmail password will not work.

To create an app password, first enable 2 factor authentication on your gmail then click on you're google profile picture and click on manage your google account. Click on the security & sign-in tab on the sidebar and search for app passwords. You should see a screen like this.

app password screen

Input your preferred app name and click create, a 16 digit password will be generated for you, copy it and add it to your .env file. Your .env file should have the following variables, EMAIL_USER = "yourmail@gmail.com" EMAIL_PASS = "trye lkit sdwr ahyg". Replace email with your email and replace password with the app password you just created. Then deploy function by running firebase deploy --only functions

If everything went well we should see our function on our google cloud console.

google cloud console

Now you can fill out your contact form and watch the magic happen. Let me know in the comment section if you're having any issues.

Top comments (0)