Express route param validation (Node.js)

March 10, 2019

If you do Node.js development you’re probably familiar with Express and maybe even Hapi. If not, they both solve the problem of server-side routing for APIs and server-rendered apps.

Update (May 25, 2019): The Hapi team have deprecated their joi package in favour of @hapi/joi so the content of the blog post has been updated to reflect that change.

Express vs. Hapi

I’m not going to get into the pros and cons of each framework, I will simply list what I love about each of them.

I love how Express is so simple and concise. It doesn’t require much setup, and its concept of middleware is straightforward and easy.

Hapi’s middleware ecosystem is similar except they’re called plugins. One great plugin is the Joi validation library. It integrates nicely with Hapi and can be used to validate route params.

Route param validation

If you’ve done any sort of back-end or full stack development, you’d know that it’s important to validate the incoming request for the right parameters. For example, perhaps you’d like to validate an ID, you’d want to make sure it’s in the format your app is looking for, e.g. auto-incrementing integer, or following a slug format, e.g. record-123abc. Or maybe you want to validate that a phone number is in the right format.

Express route param validation using Joi

I wanted to take these two great things and put them together, so two years ago I wrote the express-joi-validate library. This library allows you to use the simplicity of Express while giving you powerful route parameter validation using Joi. My library implements Express’s middleware API to process the request before it is passed off to your route handler.

Let’s say we have the following sample app in Express:

const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
const contactsHandler = require('./handlers/contacts'); // resolves the request

app.get('/contacts/:id', contactsHandler);

app.listen(port, () => {
  console.log(`Example app listening on port ${port}!`);
});

Without route parameter validation, a route in Express could look like this:

app.get('/contacts/:id', contactsHandler);

If we wanted to validate the ID param, we could do so in the handler, but arguably there’s a better way! These kinds of concerns are best delegated to middleware. With route validation using express-joi-validate, it could look like this:

const validate = require('express-joi-validate');
const Joi = require('@hapi/joi');
const contactsHandler = require('./handlers/contacts');
 
const contactSchema = {
  params: {
    id: Joi.number().required()
  }
};
 
app.get('/contacts/:id', validate(contactSchema), contactsHandler);

The above example is simple, requiring that ID be a number, but you can get really creative with validation.

If the above validation fails, the server will return 400 Bad Request automatically.

Joi comes with a bunch of validators, including lots of different string validation, e.g. email, phone number, dates, and if you like to get creative, even regular expression. Look at the Joi API reference to see what it can do.

Why do Express route param validation with Joi

You may ask why this is necessary. In my opinion, separating your API route validation from the handler makes sense in order to separate concerns.

  • Separation of concerns: validation happens outside of business logic
  • Cleaner code in your handlers
  • Don’t reinvent the wheel and write validation: this is a problem that has been solved already

This may be best illustrated with sample code.

Sample without express-joi-validate

In this example, the handler does the validation.

// route definition
const createContactHandler = require("./handlers/contact/create");

app.post("/contacts", createContactHandler);


// handler without express-joi-validate
const Contact = require("../models/contact");

const createContactHandler = (req, res) => {
  if (
    // Validate phone (optional, must match pattern)
    (req.body.phone && !req.body.phone.match(/^[0-9]*$/)) ||
    // Validate birthday (required, must match ISO date format pattern)
    (!req.body.birthdate ||
      !req.body.birthdate.match(
        /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
      )) ||
    // Validate first name (required, letters only)
    (!req.body.first_name || !req.body.first_name.match(/^[a-zA-Z]+$/))
  ) {
    return res.send(400);
  }

  return Contact.create(req.body).then(contact => {
    return res.json({ contact });
  });
};

module.exports = createContactHandler;

Sample using express-joi-validate

In this example, the middleware handles the validation, which separates the concerns and keeps the handler code clean and tidy.

// route definition
const Joi = require("@hapi/joi");
const validate = require("express-joi-validate");
const createContactHandler = require("./handlers/contact/create");

// This could be moved to a separate file to keep this even cleaner
const contactSchema = {
  body: {
    first_name: Joi.string().required(),
    birth_date: Joi.isoDate().required(),
    phone: Joi.number()
  }
};

app.post("/contacts", validate(contactSchema), createContactHandler);


// handler where express-joi-validate middleware was implemented
const Contact = require("../models/contact");

const createContactHandler = (req, res) =>
  Contact
    .create(req.body)
    .then(contact => res.json({ contact }));

module.exports = createContactHandler;

Look how clean that handler code is! The handler ends up just being the call to create the model and return it.

Joi has already solved the problem of validating dates, for example. Let’s use it instead of writing our own validation code. It’s also much nicer to read than the complex conditional statement.

How to do Express route param validation using express-joi-validate

You can download this library from NPM or from Github. See the documentation in the README file on the npm.js website.