Categories
Express Node.js

How to handle request validation in your Express API

9 min read

Last Updated: April 6, 2021

This article is part of A Guide to Express API Validation.

Let’s be real, adding request validation to your Express based API isn’t particularly exciting, but you know that it is an important foundational part of building an API, so you sit down to figure out what you’re going to do.

You try and pick a validation library, but it’s more difficult than you expect because they’re all quite different from each other, and it’s not clear what benefits one has over another. Perhaps you start to build your own custom validation, but it quickly starts to feel very messy. You just want to be able to put something reliable in place for validation and move on to building the interesting stuff in your API. You wonder to yourself, is adding request validation to an Express API really this difficult?!

In this article I’ll introduce you to JSON Schema, which allows you to describe the format that you expect data to be in and then validate data against it. I’ll then show you how to use JSON Schema to validate requests to your Express based API and send validation errors back in the response. By the time we’re done you won’t have to waste time figuring out how to handle request validation ever again.

Jump links


Important Update (Nov 20, 2020 @ 13:15 UTC): The original version of this article had code snippets where the allErrors option was being passed to Ajv. Unfortunately this particular option currently opens you up to a security vulnerability and you should not use it. All code examples have been updated to remove this option. Thanks to Matteo Collina, Lead Maintainer of the Fastify framework, for making me aware of this issue.


Getting to grips with JSON Schema

JSON Schema is very powerful, but for now we’ll only use a few of its features so that we can get comfortable with how it works.

Here’s an example JSON schema showing some of the types and keywords which you can use to describe how an object should be structured:

{
	"type": "object",
	"required": ["name"],
	"properties": {
		"name": {
			"type": "string",
			"minLength": 1
		},
		"age": {
			"type": "integer",
			"minimum": 18
		}
	}
}

The nice thing about JSON Schema is that it tends to be self-documenting, which is great for us humans who want to quickly understand what’s going on. At the same time, JSON schemas are also machine readable, meaning that we can use a JSON Schema validator library to validate the data which our application receives against a schema.

I recommend you finish reading this article before diving deeper into all of the features of JSON Schema, but if you’re keen to learn more about them right now you can jump to the handy links I’ve collected at the end.

Why should I use JSON Schema and not validation library X?

Here are the things which I think make JSON Schema a uniquely ideal tool for data validation in your Node.js application.

No library, framework or language lock-in

There are JSON Schema validation libraries available for every popular programming language.

JSON Schema doesn’t tie you to a library or a framework e.g. Joi, Yup, validate.js. These Node.js libraries all take their own approach to defining validation rules and error messages, so the things you need to learn to use them will become obsolete if they stop being developed or become deprecated.

This almost happened with the Joi validation library earlier this year, when the lead maintainer of the Hapi.js framework which it was a part of announced plans to deprecate all modules. Fortunately Joi itself seems to have been adopted by some kind souls, but it should make you think twice about committing to a specific library when more widely supported tooling is available.

Move between Node.js frameworks, or even languages, and take your schemas with you

Because JSON schemas aren’t tied to a framework, it’s one less thing to worry about if you decide to migrate away from Express to something else e.g. Fastify, which has built in support for request validation and response serialization with JSON Schema.

Because JSON Schema itself is language agnostic and widely supported, if you ever decide to rewrite your Node.js applications in a completely different language e.g. Go or Rust, you won’t need to rewrite all of the validation – you can take your JSON schemas with you!

Active and supportive community

There is an active community of folks on Slack who are very willing to help you out. The official JSON Schema website has a link which you can use to join.

JSON Schema is on a path to becoming a standard

JSON Schema is on its way to becoming a standard. It’s currently defined in a collection of IETF Internet-Draft documents, with the intention that they will be adopted by an IETF Working Group and shepherded through to RFC status, making them eligible to become an Internet Standard.

How to integrate validation with JSON schemas into your application

First things first, parse that JSON request body

Your application will need to be able to handle POST requests with a JSON body, where the Content-Type header is application/json. Here’s an example of how you can make a request like this on the command line with cURL:

curl --request POST \
  --url http://localhost:3000/user \
  --header 'Content-Type: application/json' \
  --data '{
	"first_name": "Test",
	"last_name": "Person",
	"age": true
}'

In order for the routes in our Express application to be able to handle POST requests with a JSON body, we need to configure our application to use the built-in express.json() middleware:

/**
 * You can add the `json()` middleware anywhere after you've
 * created your Express application, but you must do it before
 * you define routes which expect a JSON request body.
 *
 * If a request with a `Content-Type: application/json` header is
 * made to a route, this middleware will treat the request body as
 * a JSON string. It will attempt to parse it with `JSON.parse()`
 * and set the resulting object (or array) on a `body` property of
 * the request object, which you can access in your route handlers,
 * or other general middleware.
 */
app.use(express.json());

This functionality has been available in Express since v4.16.0. If you’re using an older version of Express you will need to install and configure the body-parser middleware package.

Integrate Ajv (Another JSON Schema Validator) into your application

The Ajv (Another JSON Schema Validator) library is the most popular JSON Schema validator written for JavaScript (Node.js and browser). You can use Ajv directly, however for an Express based API it’s nice to be able to use middleware to validate request data which has been sent to an endpoint before that endpoint’s route handler is run. This allows you to prevent things like accidentally storing invalid data in your database. It also means that you can handle validation errors and send a useful error response back to the client. The express-json-validator-middleware package can help you with all of this.

The express-json-validator-middleware package uses Ajv and allows you to pass configuration options to it. This is great as it means you have full control to configure Ajv as if you were using it directly.

Before we integrate this middleware into our application, let’s get it installed:

npm install express-json-validator-middleware

Once you have it installed you need to require it in your application and configure it:

const { Validator } = require("express-json-validator-middleware");

/**
 * Create a new instance of the `express-json-validator-middleware`
 * `Validator` class and pass in Ajv options if needed.
 *
 * @see https://github.com/ajv-validator/ajv/blob/master/docs/api.md#options
 */
const { validate } = new Validator();

Using a JSON schema to validate a response

In this next code snippet we’re going to do two things:

  1. Define a JSON schema which describes the data which we expect to receive when a client calls our API endpoint to create a new user. We want the data to be an object which always has a first_name and a last_name property. This object can optionally include an age property, and if it does, the value of that property must be an integer which is greater than or equal to 18.
  2. We’re going to use the user schema which we’ve defined to validate requests to our POST /user API endpoint.
const userSchema = {
	type: "object",
	required: ["first_name", "last_name"],
	properties: {
		first_name: {
			type: "string",
			minLength: 1,
		},
		last_name: {
			type: "string",
			minLength: 1,
		},
		age: {
			type: "integer",
			minimum: 18,
		},
	},
};

/**
 * Here we're using the `validate()` method from our `Validator`
 * instance. We pass it an object telling it which request properties
 * we want to validate, and what JSON schema we want to validate the
 * value of each property against. In this example we are going to
 * validate the `body` property of any requests to the POST /user
 * endpoint against our `userSchema` JSON schema.
 *
 * The `validate()` method compiles the JSON schema with Ajv, and
 * then returns a middleware function which will be run every time a
 * request is made to this endpoint. This middleware function will
 * take care of running the validation which we've configured.
 *
 * If the request `body` validates against our `userSchema`, the
 * middleware function will call the `next()` Express function which
 * was passed to it and our route handler function will be run. If Ajv
 * returns a validation error, the middleware  will call the `next()`
 * Express function with an error object which has a `validationErrors`
 * property containing an array of validation errors, and our route handler
 * function will NOT be run. We'll look at where that error object gets
 * passed to and how we can handle it in the next step.
 */
app.post(
	"/user",
	validate({ body: userSchema }),
	function createUserRouteHandler(request, response, next) {
		/**
		 * Normally you'd save the data you've received to a database,
		 * but for this example we'll just send it back in the response.
		 */
		response.json(request.body);

		next();
	}
);

You can validate any property in the Express request object, such as query, which contains the URL query string parsed into an object. The Validating multiple request properties section of the express-json-validator-middleware documentation provides an example of this.

Sending validation errors in a response

In the previous code snippet we learnt how to integrate the express-json-validator-middleware so that it will validate a request body against our user schema. If there is a validation error, the middleware will call the next() Express function with an error object. This error object has a validationErrors property containing an array of validation errors. When an error object is passed to a next() Express function, it automatically stops calling all regular middleware for the current request, and starts calling any error handler middleware which has been configured.

Note: When you use the code in this article the validationErrors array will only ever contain the first validation error which Ajv encounters. This is because we are not enabling the Ajv allErrors option, which introduces a security vulnerability.

The difference between error handler middleware and regular middleware is that error handler middleware functions specify four parameters instead of three i.e. (error, request, response, next). To be able to handle the error created by express-json-validator-middleware and send a useful error response back to the client we need to create our own error handler middleware and configure our Express application to use.

const { ValidationError } = require("express-json-validator-middleware");

/**
 * Error handler middleware for handling errors of the
 * `ValidationError` type which are created by
 * `express-json-validator-middleware`. Will pass on
 * any other type of error to be handled by subsequent
 * error handling middleware.
 *
 * @see https://expressjs.com/en/guide/error-handling.html
 *
 * @param {Error} error - Error object
 * @param {Object} request - Express request object
 * @param {Object} response - Express response object
 * @param {Function} next - Express next function
 */
function validationErrorMiddleware(error, request, response, next) {
	/**
	 * If response headers have already been sent,
	 * delegate to the default Express error handler.
	 */
	if (response.headersSent) {
		return next(error);
	}

	/**
	 * If the `error` object is not a `ValidationError` created
	 * by `express-json-validator-middleware`, we'll pass it in
	 * to the `next()` Express function and let any other error
	 * handler middleware take care of it. In our case this is
	 * the only error handler middleware, so any errors which
	 * aren't of the `ValidationError` type will be handled by
	 * the default Express error handler.
	 *
	 * @see https://expressjs.com/en/guide/error-handling.html#the-default-error-handler
	 */
	const isValidationError = error instanceof ValidationError;
	if (!isValidationError) {
		return next(error);
	}

	/**
	 * We'll send a 400 (Bad Request) HTTP status code in the response.
	 * This let's the client know that there was a problem with the
	 * request they sent. They will normally implement some error handling
	 * for this situation.
	 *
	 * We'll also grab the `validationErrors` array from the error object
	 * which `express-json-validator-middleware` created for us and send
	 * it as a JSON formatted response body.
	 *
	 * @see https://httpstatuses.com/400
	 */
	response.status(400).json({
		errors: error.validationErrors,
	});

	next();
}

This allows us to send back error responses like this when there is an error validating the request body against our user schema:

< HTTP/1.1 400 Bad Request
< Content-Type: application/json; charset=utf-8
< Content-Length: 187

{
    "errors": {
        "body": [
            {
                "keyword": "minimum",
                "dataPath": ".age",
                "schemaPath": "#/properties/age/minimum",
                "params": {
                    "comparison": ">=",
                    "limit": 18,
                    "exclusive": false
                },
                "message": "should be >= 18"
            }
        ]
    }
}

Pulling it all together

Here are all of the code snippets in this article combined into a complete Express API application:

const express = require("express");

const {
	Validator,
	ValidationError,
} = require("express-json-validator-middleware");

const { validate } = new Validator();

function validationErrorMiddleware(error, request, response, next) {
	if (response.headersSent) {
		return next(error);
	}

	const isValidationError = error instanceof ValidationError;
	if (!isValidationError) {
		return next(error);
	}

	response.status(400).json({
		errors: error.validationErrors,
	});

	next();
}

const userSchema = {
	type: "object",
	required: ["first_name", "last_name"],
	properties: {
		first_name: {
			type: "string",
			minLength: 1,
		},
		last_name: {
			type: "string",
			minLength: 1,
		},
		age: {
			type: "integer",
			minimum: 18,
		},
	},
};

const app = express();
app.use(express.json());

app.post(
	"/user",
	validate({ body: userSchema }),
	function createUserRouteHandler(request, response, next) {
		response.json(request.body);

		next();
	}
);

app.use(validationErrorMiddleware);

const PORT = process.env.PORT || 3000;

app.listen(PORT, () =>
	console.log(`Example app listening at http://localhost:${PORT}`)
);

Note: For the purpose of this article I’ve combined everything into one block of code, but in a real application I would recommend separating the concerns into separate files. You can read more about this in 5 best practices for building a modern API with Express.

Wrapping things up

You might have guessed from this article that I’m a big fan of JSON Schema. I think that it’s an excellent way to approach request validation, and I hope that you’re now ready to give it a try in your Express based applications.

I’ll send you my super handy JSON Schema Cheat Sheet if you drop your email address in the form below.

You can learn how to transform the raw errors array from Ajv into an even more helpful error response in my article Send awesome structured error responses with Express.

  • Understanding JSON Schema book – An excellent free online book which will teach you the fundamentals and help you make the most of JSON Schema (also available in PDF format).
  • JSON Schema Specification Links – The latest specifications for JSON Schema.
  • ajv-errors – An Ajv plugin for defining custom error messages in your schemas.
  • fluent-schema – Writing large JSON schemas is sometimes overwhelming, but this powerful little library allows you to write JavaScript to generate them.

This article is part of A Guide to Express API Validation.

6 replies on “How to handle request validation in your Express API”

Hi,
its an interesting article so thanks, first to all. Do you know if this library have in consideration params on URL and query? the article mentions is about body params.
Thanks

Hy Simon, is there a way to handle conditional logic validation with JSON Schema ? I need to validate a JSON object only if a value is true. For example:
{ “flight”: true,
“hotel”: false,
“flightReservation”: {
“type”: “depart-return”,
“flights”: [
{
“fromTown”: “Paris”,
“toTown”: “Barcelona”,
} ] }}
The “flightReservation” name will exist only if the value of “flight” is true. Because the value of “hotel” is false , there is no “hotelReservation” JSON object. Is there a way to achieve such demand with JSON Schema ? Do I find such examples in your book ? Thank you for articles, I learn a lot from them.

Hi Marius,

Thanks for your question. The good news is that JSON Schema provides support for conditional validation with the if / then / else keywords. You can read about how to use them here:

Applying subschemas conditionally
Ajv documentation for if / then / else

I hope you find those links helpful for what you’re trying to achieve. I haven’t written about conditional validation with JSON Schema before, but I will certainly consider it now.

I’m so glad to hear that you find my articles helpful!

Leave a Reply

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