dark mode

Use Express.js, React and NoSql in Firebase to create contact form

Use Express.js, React and NoSql in Firebase to create contact form

Last year I wrote the guide How to integrate Serverless contact form using Firebase Cloud functions in React. Now we will improve our contact form with Express.

Install all dependencies

The guide requires 6 dependencies.

npm install cors express express-validator firebase-admin firebase-functions nodemailer

Functions tree

After executing the firebase init functions (find more information on my previous post) we need to navigate to functions' folder and create the following folders and files.

functions
|-- api
|   \-- contacts.js
|
|-- config
|   |-- db.js
|   \-- firebase-adminsdk.json
|
|-- .gitignore
|-- .runtimeconfig.json
|-- index.js

Now let's go through all the files:

In .gitignore file

Here we prevent from uploading to the git repository all sensitive information from firebase.

node_modules/
.runtimeconfig.json
firebase-adminsdk.json

In .runtimeconfig.json file

We can generate this file with firebase (details in the previous post) or just create the JSON file and write the credentials like in the snippet below.

{
  "gmail": {
    "password": "****",
    "email": "example@mail.com"
  }
}

NB: Never write your password in plain text, even in this JSON file. Create an App password from your google account. This way you can always revoke access from everywhere. With App password, we don't need to turn off two-factor authentication.

In config > db.js and firebase-adminsdk.json file

First, we need to generate a service account key from firebase to access the database.

We can download the firebase-adminsdk.json file. Navigate to Firebase's Project settings > Service accounts > Firebase Admin SDK and press the Generate new private key button.

Next, we create db.js and write the code below. We need to replace the databaseURL with ours. To find the URL, check again the Firebase Admin SDK page. The URL is just above the Generate new private key button.

const admin = require('firebase-admin');
const serviceAccount = require('./firebase-adminsdk.json');

exports.initDb = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://webpage.firebaseio.com'
});

In index.js file

Here we add Express and set routes. We need to enable CORS because routes will be from Firebase origin and add middleware to parse incoming requests with JSON payloads. The route we define is /api/contacts and when we hit it, we call the file /api/contacts.js.

'use strict';
const functions = require('firebase-functions');
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({ origin: true }));

// Init Middleware
app.use(express.json());

// Define routes
app.use('/api/contacts', require('./api/contacts'));

exports.app = functions.https.onRequest(app);

In api > contacts.js

Here is all the back-end logic. We import all the needed libraries and files that we need.

To send a notification email, we use nodemailer. All the logic is in the sendEmail method. The settings are straight from the nodemailer documentation and we use environment variables to hide our email credentials. To set the variables, we use the following command in the terminal:

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

To check what we have set we use the command:

functions:config:get

The line will output object like:

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

We can access the password with:

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

NB: Never commit password or API keys. Almost all services support a way to hide or mask them. Search for secrets or environment variables. If you commit a sensitive information and then commit again to hide it you need to squash and rebase them and all between to erase history.

To handle a POST request to api/contacts we use router.post(). The first param is the path, and Express uses folders and filenames to construct the routes. That is why we put only '/' and not api/contacts/. For example, if we want to hit api/contacts/list, we put '/list.

Second param is a validation array, for more information check the express-validator docs. I find it more practical to use this node library than to write validation checks.

The third param is a callback function which handles the request. If there is a validation error, we notify the front-end. If all is good, we emailed a message, write the data into our database and finally notify the front-end that all is successful.

'use strict';
const functions = require('firebase-functions');
const express = require('express');
const nodemailer = require('nodemailer');
const router = express.Router();
const { check, validationResult } = require('express-validator');
const { initDb } = require('../config/db');

const db = initDb.firestore();

router.post(
  '/',
  [check('email', 'Please include a valid email').isEmail(), check('message', 'Message is required').not().isEmpty()],
  (req, res) => {
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
      res.status(400).json({ errors: errors.array() });
      return;
    }

    sendEmail(req.body);

    db.collection('contacts').add(req.body);

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

const sendEmail = (entry) => {
  const mailTransport = nodemailer.createTransport({
    service: 'gmail',
    auth: {
      user: functions.config().gmail.email,
      pass: functions.config().gmail.password
    }
  });

  mailTransport.sendMail({
    from: entry.email,
    replyTo: entry.email,
    to: functions.config().gmail.email,
    subject: `pantaley.com ${entry.email}`,
    text: entry.message,
    html: `<p>${entry.message}</p>`
  });
};

module.exports = router;

In our Front-End JS file

Here is how we make a POST request to the server. Check the route, the origin is from cloudfunctions.net and not your domain, that is why we need CORS in the back end. You can find your route when you upload the function log into firebase and navigate to Functions section.

NB: I you native fetch api, but you can use your own library or XMLHttpRequest.

Handle errors

If we have validation errors res.errors will be an array of object(s). Every object will have value, msg, param, location properties. In the back-end we set this validation for the email:

check('email', 'Please include a valid email')

In the check method, first is the param second is the msg. From here we can create a React Hook to fetch all errors and another to toggle a loading spinner.

The guide is created and tested with React and that is why it is in the title, but it is not required.

fetch('https://ourProjectName.cloudfunctions.net/app/api/contacts', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-type': 'application/json'
  },
  body: JSON.stringify(data)
})
  .then((res) => res.json())
  .then((res) => {
    if (res.errors) {
      // handle errors
    }
    if (res.isEmailSend) {
      // handle when status is 200
    }
    return res;
  })
  .catch((errors) => {
    console.log(errors);
  });

Related articles

© 2021 All rights reserved.