Categories
Express Node.js

How can you handle request validation in your Express based API?

9 min read

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 snippets have been updated to remove this option. Thanks to Matteo Collina, Lead Maintainer of the Fastify framework, for making me aware of this issue.


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


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
}'

The package most commonly used for handling the JSON body of a POST request in Express based applications is body-parser. If you already have it installed and configured in your application, that’s great, and you can skip on to the next section, otherwise let’s get it set up:

npm install body-parser

And then add it into your application:

const bodyParserMiddleware = require("body-parser");

/**
 * You can add the `body-parser` middleware anywhere after
 * you've created your Express application, but you must do
 * it before you define your routes.
 *
 * By using the `json()` method, if a request comes into your
 * application with a `Content-Type: application/json` header,
 * 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(bodyParserMiddleware.json());

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 validation errors, 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();
	}
);

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 are validation errors, 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.

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.

/**
 * 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 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 bodyParserMiddleware = require("body-parser");

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

const { validate } = new Validator();

function validationErrorMiddleware(error, request, response, next) {
	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(bodyParserMiddleware.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.

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.

In my next article I’ll be showing you how you to transform that raw errors array from Ajv into an even more helpful error response by applying the "problem detail" specification. If you want to hear from me when I publish this new article, be sure to drop your email address in the form below.

  • 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.

Leave a Reply

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