Validate HTTP Parameters & Body in Express

19 min read·Jan 1, 2025

The second and probably most important part of the data handling process after parsing is called validation, as parsing alone is not enough to guarantee that the data sent by a user or another application respects the API contract.

In this context, validation refers to the process of ensuring the accuracy of the parsed data by matching its value, type, and format against a predefined model.

Get started with Joi

Joi is a JavaScript library used for validating objects.

$ npm install --save joi

The validation of an object with Joi is a two step process:

  1. First, we need to construct the schema of this object using types and constraints.
  2. Second, we need to validate this schema against an arbitrary value, which in this case will be the parsed message body of an incoming request.

Create a validation schema

In Joi, a schema refers to a set of rules that define the structure, types, and constraints that an object must adhere to in order to be considered valid.

To create a schema, we can use the top-level object() method exported by the joi module that takes as argument an object:

const Joi = require('joi');

const schema = Joi.object({
  property: Joi.type().constraint()
});

Where:

  • property is the name of an object property (e.g. email).
  • type() is a chainable function used to specify the data type (e.g. string())
  • constraint() is a chainable function used to specify a constraint (e.g. email()).

Example

In this example, the user schema has an email and a password property:

const Joi = require('joi');

const user = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(20).required()
});

Where:

  • email is a mandatory string representing a valid email address.
  • password is a mandatory string of a length between 8 and 20 characters.

Validate data using a schema

Once the schema is defined, we can use its validate() method to verify that the data matches the described format, which returns two objects:

const { error, value } = schema.validate(data);

Where:

  • error will either contain an undefined value if the data is valid or a ValidationError otherwise.
  • value will contain the type converted values of the data object. This means that joi will automatically convert these values to match their declared type, for example, a Joi.number() will be converted to an integer, a Joi.date() will be converted to a Date object and so on.
  • data is an arbitrary JavaScript object to validate.

Example

Let's consider this module, that exports a Joi schema:

user-schema.js
const Joi = require('joi');

module.exports = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(20).required()
});

Let's consider this script, that uses the schema exported by the user-schema.js module to validate objects:

index.js
const userSchema = require('./user-schema.js');

const userA = {
  email: 'johndoe@mail.com',
  password: 'helloworld'
};

console.log(userSchema.validate(userA));

const userB = {
  email: 'johndoe',
  password: ''
};

console.log(userSchema.validate(userB));

Which will produce this output:

$ node index.js
validation 1 { value: { email: 'johndoe@mail.com', password: 'helloworld' } }
validation 2 {
  value: { email: 'johndoe', password: '' },
  error: [Error [ValidationError]: "email" must be a valid email] {
    _original: { email: 'johndoe', password: '' },
    details: [ [Object] ]
  }
}

Create a payload validation middleware

In Express, the role of a payload validation middleware is to streamline the validation process by checking the data parsed from the incoming HTTP request, and either:

  1. Break the request-response cycle by sending an error response back to the client if the data doesn't match (e.g., missing fields, invalid data types).
  2. Forward the request to the next component in the route's call stack.

This helps avoid unnecessary processing at the controller level and mitigate the risk of potential errors due to incorrect or invalid data.

Create a schema directory

The first step consists in creating a directory that contains all the schema definitions used by the middleware for the entire application, and within this directory, an index file in charge of exporting the schemas as a single object.

Example

Let's create a new directory named schemas:

$ mkdir schemas

Within this directory, let's create a new file named signup.js that exports a schema for validating the payload sent to a sign up endpoint:

schemas/signup.js
const Joi = require('joi');

const signupSchema = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(20).required(),
});

module.exports = signupSchema;

Within this directory, let's also create a new file named index.js that exports an object containing the signup schema:

schemas/index.js
const signupSchema = require('./signup');

module.exports = {
  signup: signupSchema
};

Create a validation middleware

Let's create a new directory named middlewares:

$ mkdir middlewares

Within this directory, let's create a new file named payload-validator.js that exports a higher-order function that takes as argument the name of the schema we want to use and returns a middleware function:

middlewares/payload-validator.js
const schemas = require('../schemas');

const payloadValidator = (schemaName) => (req, res, next) => {
  //
};

module.exports = payloadValidator;

Let's now verify whether the specified schemaName is a valid key of the schemas object and respond with an HTTP 500 Internal Server Error if mistakes were made on the developer's end, such as an oversight of schema definition or a typo in the schema's name.

middlewares/payload-validator.js
const schemas = require('../schemas');

const payloadValidator = (schemaName) => (req, res, next) => {
  const schema = schemas[schemaName] || null;

  if (schema) {
    // 
  } else {
    res.sendStatus(500);
  }
};

module.exports = payloadValidator;

Finally, if the specified schema exists, the middleware will call the schema's validate() method with the request's payload contained in the req.body object, and either:

  1. Break the request-response cycle by sending a HTTP 400 Bad Request if the payload is invalid.
  2. Forward the request to the next component in the call stack using the next() function otherwise.
middlewares/payload-validator.js
const schemas = require('../schemas');

const payloadValidator = (schemaName) => (req, res, next) => {
  const schema = schemas[schemaName] || null;

  if (schema) {
    const { error } = schema.validate(req.body);

    if (error) {
      res.sendStatus(400);
    } else {
      next();
    }
  } else {
    res.sendStatus(500);
  }
};

module.exports = payloadValidator;

Use the validation middleware

To use the validation middleware on a specific route, you can invoke it right after the parsing middleware using one of the schema names exported by the schemas/index.js file:

app.METHOD(PATH, PARSER, payloadValidator('schemaName'), HANDLER);

Where:

  • PARSER is a parsing middleware function, like express.json().
  • HANDLER is a request listener function.

Example

Let's consider this directory structure, that represents the files we've just created:

~/projects/
└── server/
   ├── app.js
   ├── middlewares/
   |  └── payload-validator.js
   └── schemas/
      ├── index.js
      └── signup.js

Let's consider this server, that implements a single HTTP POST /signup route and uses the payloadValidator middleware:

app.js
const express = require('express');
const payloadValidator = require('./middlewares/payload-validator');

const app = express();

app.post('/signup', express.json(), payloadValidator('signup'), (req, res) => {
  res.sendStatus(200);
});

app.listen(3000);

When sending this request, the server should responds with an HTTP 200 OK:

$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"name":"John Doe","email":"johndoe@mail.com","password":"helloworld"}' \
127.0.0.1:3000/signup
OK

When sending this request, the server should respond with an HTTP 400 Bad Request caused by the missing name field in the request's payload:

$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"johndoe@mail.com","password":"helloworld"}' \
127.0.0.1:3000/signup
Bad Request

Summary

Here's a summary of what you've learned in this lesson:

  • Validation refers to the process of ensuring the accuracy of the parsed data.
  • The Joi.object() method is used to create a validation schema.
  • The validate() method of a schema is used to check arbitrary data against the format described by the schema.

Enjoying the courses?

I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:

Support BackendBrewery
Validate HTTP Parameters & Body in Express.js | Backend Brewery