Build a contact form with Node.js and nodemailer

It's known that working with Node.js without any frameworks is tricky and takes a lot of effort. As a developer, you should know how everything work then use it. Working with tools and technologies that you don't know how it is working will make you just a coder not a developer.

What we'll do

In this article we'll learn node.js concepts by building a contact form application that serves a simple form, gets data from it then sends an email via Gmail service. So you need to know just the basics of node.js and the web life cycle (request, response & client, server).

Also you should have node.js & npm installed in your machine. To confirm it's installed run:

$ node -v
v12.13.0

$ npm -v
6.12.0

What we'll learn

  1. Handling requests coming from client (GET / POST) and headers.
  2. Serving static files (html & js).
  3. Use fetch and how to handle the request.
  4. Use nodemailer package and send emails using gmail service.

Design the application

To be more productive, you should design the app before beginning on it. Asking questions like:

  • What does the problem this app solve ?
  • Who is this app for ?
  • What's this app consists of (pages, file structure, components, ...etc) ?
  • What's the steps we'll go through to build the app ?

Now let's start the design and answer these questions ...

  1. What does the problem this app solve ?

A contact form used for a lot of purposes like let people contact with us in a portfolio website for example. Let's take this purpose as the problem we're gonna solve.

  1. Who is this app for ?

What's the type of users that'll use this app. Is this app for clients or developers, companies or individuals, ...etc. For the purpose of this tutorial we'll consider the users as clients.

  1. What's this app consists of ?

According to the previous questions and answers, the app is a contact form for a freelancer and the users are the clients. So it'll be simple as the freelancer only want to know the name, email and the message.

  1. What's the steps we'll go through to build the app ?
  2. Init the project and put the file structure.
  3. Create a server and listen to it.
  4. Build the html file that contains a bootstrap form.
  5. Serve the html file from the server.
  6. Link the js file with the html and serve it from the server.
  7. POST the form data from the front with fetch.
  8. Handle the POST request from the server and use nodemailer to send emails.

Now, after we designed our software, let's start the implementation phase.

Init the project and put the file structure

To initialize the project you should create a package.json file that manages the packages of the app. Run the following command in the terminal:

$ npm init -y

Notice the -y here, it just creates the file with initial values.

Now let's create the file structure:

root/
    |__ package.json
    |__ index.js
    |__ public/
        |__ index.html
        |__ main.js

root

It's the root folder of the app.

package.json

The manager of the app.

public/

The folder that contains the static files html & js.

Create a server and listen to it

Now edit the index.js file to create a server and listen to it:

  1. require http package.
const http = require("http");
  1. create the server.
const server = http.createServer((request, response) => {});
  1. Listen to the server on port 8000.
// Listen to the server
// You can specify any port u like
server.listen(8000, () => {
  console.log("server is listening on port 8000");
});

Build the html file that contains a bootstrap form

In the index.html file write the following code that calls bootstrap and the js file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Contact Form</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
      integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div class="container">
      <h1>Contact Form</h1>
      <form id="contact">
        <div class="form-group">
          <label for="email">Email address</label>
          <input type="email" class="form-control" id="email" name="email" />
        </div>
        <div class="form-group">
          <label for="name"> Your Name </label>
          <input type="text" class="form-control" id="name" name="name" />
        </div>
        <div class="form-group">
          <label for="message"> Your Message </label>
          <textarea name="message" class="form-control" id="message" cols="30" rows="10"></textarea>
        </div>
        <button type="submit" class="btn btn-primary submit-btn">Send</button>
      </form>
    </div>
  </body>
</html>

Serve the html file from the server

What's that mean ? let's see the client-server communication first.

Before diving into this communication, you need to know that the website is just a files and these files are stored in the server and then the browser downloads it as we'll know now.

This communication happens through some steps:

  1. The browser sends http request to the server to get the files of the website with the url /.
  2. The server receives the request then sends the html file to the browser.
  3. The browser receives the html file then starts to render it line by line.
  4. When the browser hits a line in html like , it sends another request to get this file, then the cycle goes again.

We'll benefit from this concept to serve static files.

To serve the html file, we'll send the html file when the browser sends the request "/", so we'll check the request url

// Get the html file beside the server in the 'public' folder in the file structure and cache it in a variable to use it later
let htmlFile = path.resolve(__dirname, "public/index.html");

const server = http.createServer((request, response) => {
  // Send the html file when the request url is '/'
  if (request.url == "/") {
    // First set the headers to let the browser knows it's an html file
    response.setHeader("Content-type", "text/html");

    // Send the html file by creating a read stream and pipe it to the resoponse object
    fs.createReadStream(indexFile).pipe(response);
  }
});

Now our code looks like this:

const http = require("http");
const fs = require("fs");
const path = require("path");

// Cache the static files
let indexFile = path.resolve(__dirname, "public/index.html");

// Create the server
let server = http.createServer((req, res) => {
  // send the html file
  if (req.url == "/") {
    // set the header
    res.setHeader("Content-Type", "text/html");
    // send the html file
    fs.createReadStream(indexFile).pipe(response);
  }
});

// Listen to the server
// You can specify any port u like
server.listen(8000, () => {
  console.log("server is listening on port 8000");
});

Link the js file then serve it from the server

Add this to the bottom of the body of html file

<script type="text/javascript" src="/public/main.js"></script>

Then serve the js file from the server

// Get the js file and cache it in a variable to use it later
let jsFile = path.resolve(__dirname, "public/main.js");

const server = http.createServer((request, response) => {
  ...

  // We'll send the js file by checking the extension of the file
  if (path.extname(request.url) == ".js") {
    // Send the js file
    fs.createReadStream(jsFile).pipe(response);
  }
});

POST the form data from the front with fetch

We'll use the Fetch API, you can learn more about it here

In the js file we'll handle the submit event of the form

// Select the form element
const formElement = document.querySelector("#contact");

// Attach the submit event to the form
formElement.addEventListener("submit", function(e) {
  e.preventDefault();
});

We'll convert the form data into JSON with this convertFormIntoJSON function

/**
 * @param {HTMLElement} form
 * @returns {JSON}
 */
function convertFormIntoJSON(form) {
  let formObject = {};

  // Use querySelectorAll to get all inputs and textareas in the form
  let formFields = form.querySelectorAll("input, textarea");

  // Using forEach to produce an object of [name] of the field as property and value of the field
  formFields.forEach(input => {
    formObject[input.name] = input.value;
  });

  return JSON.stringify(formObject);
}

Then use it inside the submit event handling.

formElement.addEventListener("submit", function(e) {
  e.preventDefault();

  // 'this' refers to the formElement
  let reqBody = convertFormIntoJSON(this);
});

Post the data through Fetch with the postFormData function that takes the url and the body to send then get a callback function on success using Promise.

/**
 * @param {string} url
 * @param {JSON} body
 * @param {string} successCallBack
 */
function postFormData(url, body, successCallBack) {
  fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json" // Set the header to json as the body will be json
    },
    body: body
  }).then(successCallBack);
}

Then send it in the submit event

formElement.addEventListener("submit", function(e) {
  e.preventDefault();

  // 'this' refers to the formElement
  let reqBody = convertFormIntoJSON(this);

  // Send the form to "/" because it's the origin
  postFormData("/", reqBody);
});

I put all of this code in a repo with more features so look at it to learn more about how we can handle the requests through js.

Handle the POST request from the server and use nodemailer to send emails.

We'll handle the POST request through checking the method of the request and the url.

// Get the js file and cache it in a variable to use it later
let jsFile = path.resolve(__dirname, "public/main.js");

const server = http.createServer((request, response) => {
  ...

  // We'll send the js file by checking the extension of the file
  if (request.method == "POST" && request.url == "/") {
    // The data comes from the front-end is a buffer, so we'll handle this with 'data' event and convert buffer into object
    request.on("data", chunk => {
      // Convert buffer into object
      let bodyObject = JSON.parse(data.toString());
    })
  }
});

Now install the nodemailer package to use it.

$ npm i nodemailer

Then, require the package:

const nodemailer = require("nodemailer");

Then, create the transporter (we'll use the gmail as an email service):

let transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: "yourEmail@gmail.com",
    pass: "passwordOfYourAccount"
  }
});

Now, our server index.js file looks like:

const http = require("http");
const fs = require("fs");
const path = require("path");

const nodemailer = require("nodemailer");

let transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    user: "yourEmail@gmail.com",
    pass: "passwordOfYourAccount"
  }
});

// Cache the static files
let indexFile = path.resolve(__dirname, "public/index.html");
let jsFile = path.resolve(__dirname, "public/main.js");

// Create the server
let server = http.createServer((req, res) => {
  // send the html file
  if (req.url == "/") {
    // set the header
    res.setHeader("Content-Type", "text/html");
    // send the html file
    fs.createReadStream(indexFile).pipe(res);
  }

  if (path.extname(req.url) == ".js") {
    // Send the js file
    fs.createReadStream(jsFile).pipe(res);
  }

  // Get the contact form data
  if (req.method == "POST") {
    // 'data' is a buffer
    req.on("data", data => {
      // Convert buffer into object
      let bodyObject = JSON.parse(data.toString());
    });
  }
});

// Listen to the server
// You can specify any port u like
server.listen(8000, () => {
  console.log("server is listening on port 8000");
});

Then, we'll send the email with data comes from the front-end:

const server = http.createServer((request, response) => {
  ...

  // We'll send the js file by checking the extension of the file
  if (request.method == "POST" && request.url == "/") {
    // The data comes from the front-end is a buffer, so we'll handle this with 'data' event and convert buffer into object
    request.on("data", chunk => {
      // Convert buffer into object
      let bodyObject = JSON.parse(data.toString());

      let mailOptions = {
        from: "yourEmail@gmail.com",
        to: "yourEmail@gmail.com",
        subject: `Message from ${bodyObject.name}`,
        text: `
            ${bodyObject.message}
            from: ${bodyObject.email}
        `
      };

      // Send to email
      transporter.sendMail(mailOptions, (err, info) => {
        if (err) console.log(err);

        console.log(`Email Sent`);
      });
    })
  }
});

This is all of it.

Conclusion

In this tutorial we made a contact form with Node.js, learned how to design the app and learned concepts like client-server communication which is very important for any web developer to know.

Sources

  1. The repo.
  2. Nodemailer from w3schools.
  3. Also u can use sendgrid as the email service instead of gmail, which has more features and here's how to do this.
  4. Fetch API.