How to create a simple contact form with Node.js?

Project initialization.

Let’s start by creating an empty folder and initializing our package.json with npm init. We will also include express to set up our application, express-validator to validate the form submission, and mustache (mustache-express) to render the form.

npm init -y
npm i --save express express-validator mustache-express

The basics.

With our packages ready, let’s go on and create our index.js. We will start by including our packages and creating the express app with the express router:

const path = require('path');
const { body, validationResult } = require('express-validator');
const express = require('express');
const mustacheExpress = require('mustache-express');

const port = 8000;
const app = express();
const router = express.Router();

// process POST forms
app.use(express.urlencoded({ extended: true }));

For this example, our router will be as simple as one controller for GET and one for POST of the home route (we will work on the POST controller later in the course):

router.get('/', (req, res, next) => {
  res.render('home');
});

With this in place, we need to configure what and where is going to render our home template. We need to register our mustache engine, and configure it to look for templates in our views folder:

// html rendering
app.engine('mustache', mustacheExpress());
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'mustache');

We are good with HTML rendering, and we can now continue on with the styles.

Setting up the public folder, and preparing Tailwind CSS.

Let’s add another app.use to mark the folder as public, and to serve all the resources directly from that folder.

// public files
app.use(express.static(path.join(__dirname, 'public')));

After the static folder is set, let’s go back to the command line and produce a full version of Tailwind CSS. This is the least recommended way (as it produces around 4MB of CSS, out of which you might use like 1-2KB), but we are mostly doing this to see how to handle the forms in Node.js, so let’s continue:

npx tailwindcss -o public/css/tailwind.css

Now that our public folder is setup, and we have a base Tailwind CSS, all we need to do is use our router and run the server:

// controllers
app.use(router);

// run!
app.listen(port, () => {
  console.log(`Server started at port ${port}`)
});

Tip: while working on templates, it might be useful to run nodemon to watch your file changes and reload the server automatically, so include mustache into the list of extensions as well, such as:

nodemon -e js,mustache

Handling the form and validating submission.

With all the setup done, we can start writing the POST controller. It should have a few key elements: validator middleware, displaying the errors, rendering previously input value, and displaying the success message. Let’s start with very very basic validation, we’ll check that both fields are at least 2 characters long:

router.post('/', 
  body('name').isLength({ min: 2 }).withMessage('Please input your name'),
  body('message').isLength({ min: 2 }).withMessage('Please input your message'),
  (req, res, next) => {
    const errors = validationResult(req);
    ...

Here we use the body middleware from express-validator, and the validatorResult to collect all the errors into an array. If there are any errors, we render the same form again, but add the errors array and the values that we just sent:

if (!errors.isEmpty()) {
  return res.status(400).render('home', { errors: errors.array(), name: req.body.name, message: req.body.message });
}

Now that we validated our fields, we can use our fields (send them via e-mail, save to database etc.). After we processed our fields are in req.body, we can render the thankyou template:

// req.body.name
// req.body.message

// render thank you
res.render('thankyou');

The form layout

I won’t describe the visual aspects of the template, will only note that there is an #errors loop to output the validation errors, and there’s a render of field values in case there was a validation error and we don’t want to lose our input:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Contact Us</title>
  <link rel="stylesheet" href="/css/tailwind.css">
</head>
<body>
  <div class="container m-auto grid grid-cols-12">
    <div class="col-span-12 lg:col-span-6 lg:col-start-4 px-10 py-20">
      <h1 class="text-3xl font-bold text-center mb-3">Contact Us</h1>
      {{#errors}}
        <div class="text-red-600 p-2 bg-red-100 mb-2 rounded">{{msg}}</div>
      {{/errors}}
      <form action="/" method="post" class="bg-yellow-100 p-3 rounded-md shadow-2xl">
        <div class="mb-3">
          <label for="name">Your name: </label><br />
          <input id="name" type="text" name="name" class="p-3 block w-full border rounded-md shadow-inner focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" value="{{ name }}" />
        </div>
        <div class="mb-3">
          <label for="name">Message: </label><br />
          <textarea id="message" type="text" name="message" class="p-3 block w-full border rounded-md shadow-inner focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent" value="{{ message }}" ></textarea>
        </div>
        <div>
          <input type="submit" value="Send" class="text-white py-3 px-6 cursor-pointer rounded-md shadow bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50">
        </div>
      </form>
    </div>
  </div>
</body>
</html>

The thank you page can just be a very basic static html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Thank You</title>
  <link rel="stylesheet" href="/css/tailwind.css">
</head>
<body>
  <div class="container m-auto grid grid-cols-12">
    <div class="col-span-12 lg:col-span-6 lg:col-start-4 px-10 py-20">
      <h1 class="text-3xl font-bold text-center mb-3">Thank you!</h1>
      <p class="text-center">Your message has been sent</p>
    </div>
  </div>
</body>
</html>

And, here is the full code of the app:

const path = require('path');
const { body, validationResult } = require('express-validator');
const express = require('express');
const mustacheExpress = require('mustache-express');

const port = 8000;
const app = express();
const router = express.Router();

// process POST forms
app.use(express.urlencoded({ extended: true }));

// html rendering
app.engine('mustache', mustacheExpress());
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'mustache');

// public files
app.use(express.static(path.join(__dirname, 'public')));

// controllers
router.get('/', (req, res, next) => {
  res.render('home');
});

router.post('/', 
  body('name').isLength({ min: 2 }).withMessage('Please input your name'),
  body('message').isLength({ min: 2 }).withMessage('Please input your message'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).render('home', { errors: errors.array(), name: req.body.name, message: req.body.message });
    }
    // do something with the fields
    // req.body.name
    // req.body.message

    // render thank you
    res.render('thankyou');
  }
);

app.use(router);

// run!
app.listen(port, () => {
  console.log(`Server started at port ${port}`)
});

Download the source together with the template files:


Did you know that I made an in-browser image converter, where you can convert your images without uploading them anywhere? Try it out and let me know what you think. It's free.

Leave a Comment

Your email address will not be published. Required fields are marked *