dark mode

How to integrate Serverless contact form using Firebase Cloud functions in React

How to integrate Serverless contact form using Firebase Cloud functions in React

New version with Express.js

To create serverless contact forms, you need a service like Google Cloud Functions and its powerful cloud functions. In this tutorial I will show you how to create a contact form with Firebase and React. Our contact form will send us notification emails and will write an entry in Firebase Database, so we won't delete or lose messages.

Setting up Firebase

First, you will need a Google Account and sign in into the Firebase console admin panel. Next step is to create/add a project from which you will have access to a Hosting, Realtime database, Cloud functions, etc. We can find here the list of all the Firebase products. Finally, we need to initialize Firebase in our project. You have two options:

  1. If we need to choose Firebase services, like functions, hosting and database, execute in the console the command (this should be our case):

    firebase init

    After that follow the instruction and check the services we want to use like functions, database, hosting, etc.

  2. If we will use only functions just type in the console:

    firebase init functions

We can find a detailed guide in the official docs.

Our contact form will send us notification emails and will write an entry in our Firebase Database.

Start writing code

Back-end

We will start with the back-end part. Firebase uses NodeJS server and to write code, you need to navigate to functions folder and open index.js. We can find the folder after functions initialization mentioned above.

The packages we need to continue are firebase-functions, nodemailer, cors. First one is to initiate functions, the second one is to help send emails and the last one is to enable Cross-origin (CORS) requests. We will use the last one only if we will use custom domain.

As an email service we will use Gmail. It's the easiest to set and we already have an account for firebase. To protect our email and password, we will take advantage of Environment configuration, we don't want to expose our credentials. Open the terminal in the root folder of the project and type:

firebase functions:config:set gmail.email="<user@gmail.com>" gmail.password="<pass>"

This will create an object:

{
  "gmail": {
    "email": "user@gmail.com",
    "password": "pass"
  }
}

And we can access it like this:

const gmailPassword = functions.config().gmail.password;

To create Firebase function, we need to choose a trigger. In our case we will use HTTP trigger, but there are plenty of others the list is here.

exports.submit = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    // write code
  });
});

With this code snippet above we have a function called submit and is triggering from GET, POST, PUT, DELETE, and OPTIONS request. Firebase will create a special URL we can see in the Functions page.

Functions - Firebase console

Our working barebone function will look something like:

const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const cors = require('cors')({ origin: true });
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const mailTransport = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: gmailEmail,
    pass: gmailPassword
  }
});

exports.submit = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    if (req.method !== 'POST') {
      return;
    }

    const mailOptions = {
      from: req.body.email,
      replyTo: req.body.email,
      to: gmailEmail,
      subject: `from my website ${req.body.email}`,
      text: req.body.message,
      html: `<p>${req.body.message}`
    };

    mailTransport.sendMail(mailOptions);

    res.status(200).end();
    // or you can pass data to indicate success.
    // res.status(200).send({isEmailSend: true});
  });
});

What we did is to send to ourselves email with data from a POST request. From here we can extend our function to test if data typed by the user is correct or attempt to breach security or whatever we want.

Thanks to Patrick Tobias and Vips to point out we need to terminate our http functions. In the example above we use .end(), but we can pass data with .send(). For example, we can use variable isEmailSend to show and hide loading indicator.

Now we need to write the POST request for the client side.

Front-end

Let's build a simple form with email and message fields:

<form onSubmit="{handleSubmit}">
  <label>
    e-mail:
    <br />
    <input type="email" name="email" value="{email}" onChange="{handleEmailChange}" />
  </label>
  <br />
  <label>
    Message
    <br />
    <textarea type="text" name="message" value="{message}" onChange="{handleMessageChange}" />
  </label>
  <br />
  <input type="submit" value="submit" />
</form>

Then we need to initialize firebase app in the componentDidMount() or in useEffect(() => {},[]). Install the Firebase package:

npm install firebase --save

And import it:

import firebase from 'firebase/app';

We will create a config object and the data can be see from our Firebase admin panel:

const config = {
  apiKey: '****************',
  databaseURL: 'https://ourprojectname.firebaseio.com'
};

Then we initialize the app:

if (!firebase) {
  setFirebaseApp(firebase.initializeApp(config));
}

Pro Tip

At the time of writing this blog post Firebase npm package is version 5.8.3. The package is referring to the global object window which is a problem. The problem is that if we are using an environment that has no window object for example site generator like Gatsby or whatever with NodeJS we will get an error :

window.self is not defined.

If you bumped to this error, there is a simple workaround. Don't import firebase from 'firebase/app';. And initialize the app like that:

if (!firebaseApp && window !== undefined) {
  const firebase = require('firebase/app');
  setFirebaseApp(firebase.initializeApp(config));
}

END of Pro Tip

Don’t forget to check if the app has only one instance.

We will focus on the handleSubmit method which will fire when we submit the contact form.

First, we will create an object which will have the email and message and go to the back-end, something like that:

const data = { email, message };

Remember our back-end function? It takes request and response arguments. We can get the data object via request.body or in our case req.body, check the function code above for reference.

Second, we will use Axios lib for the Ajax request, but you are free to use something else.

npm i axios --save

Axios POST request has two arguments, first is the route and second is the data object. The route is the Firebase function URL mentioned above and the data is our object we created earlier.

Our barebone method will look something like:

const handleSubmit = event => {
  event.preventDefault();

  const data = { email, message, time: getTime() };

  Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data).catch(error => {
    console.log(error);
  });
};

It's fine if we stop here, but we want to not only send email to ourselves, but to add an entry to our Firebase Database.

To do that, we will chain the Ajax request with .then() method and handle the case when the request is successful.

const handleSubmit = event => {
  event.preventDefault();

  const data = { email, message, time: getTime() };

  Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data)
    .then(res => {
      // here will be code
    })
    .catch(error => {
      console.log(error);
    });
};

Don't forget the Firebase's database import:

import 'firebase/database';

Our barebone method will look something like:

const handleSubmit = event => {
  event.preventDefault();

  const data = { email, message, time: getTime() };

  Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data)
    .then(res => {
      if (firebase) {
        return firebase
          .database()
          .ref('contacts')
          .push(data);
      }
    })
    .catch(error => {
      console.log(error);
    });
};

This will create a table called contacts and push the data object to the Firebase Database.

At the end we have a working Contact form which will notify you via email and save a history of all the entries in a database.

Final code: functions/index.js

'use strict';

const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const cors = require('cors')({ origin: true });
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const mailTransport = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: gmailEmail,
    pass: gmailPassword
  }
});

exports.submit = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    if (req.method !== 'POST') {
      return;
    }

    const mailOptions = {
      from: req.body.email,
      replyTo: req.body.email,
      to: gmailEmail,
      subject: `from my website ${req.body.email}`,
      text: req.body.message,
      html: `<p>${req.body.message}`
    };

    mailTransport.sendMail(mailOptions);

    res.status(200).send({ isEmailSend: true });
  });
});

Related articles

© 2021 All rights reserved.