Summary: in this tutorial, you will learn how to validate a request’s body, parameters, and query strings using a third-party package Zod.
An Express application often receives data from the client, but you should trust this data, especially when it comes from untrusted sources.
To keep your application secure, it is crucial to always validate and sanitize the input data before processing it. One of the popular libraries for validation is Zod.
In this tutorial, we’ll show you how to use Zod to validate input data for routes including request parameters, query strings, and body.
We’ll continue the project we developed from the Express Router tutorial.
First, download the project and extract it.
Second, install Zod
by running the following command in your terminal within the project directory:
npm install zod
Code language: JavaScript (javascript)
Validating parameters
Here’s the route GET todos/:id
that retrieves a todo item by an id:
router.get('/:id', (req, res) => {
const { id } = req.params;
res.send(`Get the todo item with id ${id}`);
});
Code language: JavaScript (javascript)
If you send the following request:
GET http://localhost:3000/todos/abc
Code language: JavaScript (javascript)
… you’ll get the following response:
Get the todo item with id abc
Code language: JavaScript (javascript)
This is incorrect because the id should be a positive integer. So, we need to validate the id to protect the route from getting bad data.
Adding Zod validation
The following shows how to use the Zod library to validate the id parameter:
router.get('/:id', (req, res) => {
const schema = z.object({
id: z.coerce.number().int().positive(),
});
try {
const result = schema.parse(req.params);
req.params = result;
} catch (err) {
if (err instanceof ZodError) {
return res.status(400).json({ error: 'Invalid data', details: err });
}
// handle other errors
return res.status(500).json({ error: 'Internal Server Error' });
}
const { id } = req.params;
res.send(`Get the todo item with id ${id}`);
});
Code language: JavaScript (javascript)
How it works:
First, import z
and ZodError
objects from the zod
library:
import { z, ZodError } from 'zod';
Code language: JavaScript (javascript)
Second, define a schema to validate the id field:
const schema = z.object({
id: z.coerce.number().int().positive(),
});
Code language: JavaScript (javascript)
In this schema, we coerce the id as a number and ensure it is a positive integer.
Third, call the parse()
method of the schema object to parse parameter data that comes from the request object:
const result = schema.parse(req.params);
Code language: JavaScript (javascript)
If the validation passes, the parse()
method will return the value with the type defined in the schema. More specifically, it will return id as a positive integer.
Fourth, update the params
property of the request
object with the value that comes from the result of the parse()
method:
req.params = result;
Code language: JavaScript (javascript)
Fifth, if the validation fails, the parse()
method will throw an error with the type of ZodError
. We can catch the error using the try…catch statement and handle the ZodError
more specifically:
if (err instanceof ZodError) {
return res.status(400).json({ error: 'Invalid data', details: err });
}
Code language: JavaScript (javascript)
Finally, return an internal server error to the client if other types of errors occur:
return res.status(500).json({ error: 'Internal Server Error' });
Code language: JavaScript (javascript)
Testing Zod validation
The following request returns an error because id is not a number:
GET http://localhost:3000/todos/abc
Code language: JavaScript (javascript)
Error:
{
"error": "Invalid data",
"details": {
"issues": [
{
"code": "invalid_type",
"expected": "number",
"received": "nan",
"path": [
"id"
],
"message": "Expected number, received nan"
}
],
"name": "ZodError"
}
}
Code language: JavaScript (javascript)
The following request returns an error because id is not a positive integer:
GET http://localhost:3000/todos/0
Code language: JavaScript (javascript)
Error:
{
"error": "Invalid data",
"details": {
"issues": [
{
"code": "too_small",
"minimum": 0,
"type": "number",
"inclusive": false,
"exact": false,
"message": "Number must be greater than 0",
"path": [
"id"
]
}
],
"name": "ZodError"
}
}
Code language: JavaScript (javascript)
The following request is valid and return HTTP status code 200:
GET http://localhost:3000/todos/1
Code language: JavaScript (javascript)
The validation works as expected, but the code is a bit verbose. It also mixes validation logic with the route handler’s logic, making it difficult to maintain.
To improve this, we can refactor by creating a middleware function called validate
and use it in the route like this:
router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
const { id } = req.params;
res.send(`Get the todo item with id ${id}`);
});
Code language: JavaScript (javascript)
Creating a Zod middleware for Express
Step 1. Create a new directory middleware with the project directory:
mkdir middleware
Code language: JavaScript (javascript)
Step 2. Create a new file validation.js
within the middleware
directory with the following code:
import { ZodError } from 'zod';
export const validate = (schemas) => {
return (req, res, next) => {
for (const [key, schema] of Object.entries(schemas)) {
try {
// parse the input data
const result = schema.parse(req[key]);
req[key] = result;
next();
} catch (err) {
// handle validation error
if (err instanceof ZodError) {
return res.status(400).json({ error: 'Invalid data', details: err });
}
// handle other errors
return res.status(500).json({ error: 'Internal Server Error' });
}
}
};
};
Code language: JavaScript (javascript)
How it works.
First, import ZodError
object from the zod
library:
import { ZodError } from 'zod';
Code language: JavaScript (javascript)
Second, define a function called validate
that validates request data including body, parameters, and query strings:
export const validate = (schemas) => {
Code language: JavaScript (javascript)
The validate()
function accepts a schemas
parameter, which is an object that has keys available in the request object:
- body
- params
- query
The value of each key is a Zod schema that defines the validation rules.
The following example shows a schemas
parameter that can be used to validate both the query string and the request’s body of the todo
resource:
{
query: z.object({
id: z.coerce.number().int().positive(),
}),
body: z.object({
title: z.string(),
completed: z.boolean(),
}),
};
Code language: JavaScript (javascript)
In this example:
- Validate if the query string has the id that is a positive integer.
- Validate the body if it contains a title with the type string and completed with the type boolean.
Third, return a middleware function from the validate()
function:
export const validate = (schemas) => {
return (req, res, next) => {
Code language: JavaScript (javascript)
Fourth, iterate over the key/value pairs, each consisting of the key and schema defined in the schemas
parameter:
for (const [key, schema] of Object.entries(schemas)) {
Code language: JavaScript (javascript)
Fifth, parse the request data that comes from a key
, which can be body
, params
, or query
property of the request object:
const result = schema.parse(req[key]);
Code language: JavaScript (javascript)
Sixth, update the request’s data if the validation passes and call the next()
middleware function:
req[key] = result;
next();
Code language: JavaScript (javascript)
Seventh, if the validation fails, the parse will throw a ZodError
, we can handle it more specifically in the catch
block:
} catch (err) {
// handle validation error
if (err instanceof ZodError) {
return res.status(400).json({ error: 'Invalid data', details: err });
}
Code language: JavaScript (javascript)
Finally, return the internal server error if other errors occur:
return res.status(500).json({ error: 'Internal Server Error' });
Code language: JavaScript (javascript)
Defining Zod schema
Step 1. Create a schemas directory within the project directory:
mkdir schemas
Code language: JavaScript (javascript)
Step 2. Define Zod schema for validating todo object and todo Id:
import { z } from 'zod';
export const todoSchema = z.object({
title: z.string(),
completed: z.boolean(),
});
export const todoIdSchema = z.object({
id: z.coerce.number().int().positive(),
});
Code language: JavaScript (javascript)
Using the Zod express middleware
Modify the routes/todo.js
in the routes directory to use the validate function from the middleware/validation.js
module and Zod schemas from the schema/todo.js
module:
import express from 'express';
import { validate } from '../middleware/validation.js';
import { todoSchema, todoIdSchema } from '../schemas/todos.js';
const router = express.Router();
router.get('/', (req, res) => {
res.send('Get all todo items');
});
router.post('/', validate({ body: todoSchema }), (req, res) => {
res.send('Create a new todo item');
});
router.put(
'/:id',
validate({ params: todoIdSchema, body: todoSchema }),
(req, res) => {
const { id } = req.params;
res.send(`Update the todo item with id ${id}`);
}
);
router.delete('/:id', validate({ params: todoIdSchema }), (req, res) => {
const { id } = req.params;
res.send(`Delete the todo item with id ${id}`);
});
router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
const { id } = req.params;
res.send(`Get the todo item with id ${id}`);
});
export default router;
Code language: JavaScript (javascript)
How it works.
First, import validate
function from '../middleware/validation.js'
and todoSchema
, and todoIdSchema
from the '../schemas/todos.js'
module:
import { validate } from '../middleware/validation.js';
import { todoSchema, todoIdSchema } from '../schemas/todos.js';
Code language: JavaScript (javascript)
Second, validate the body of the request for the route GET /todos based on the todoSchema:
router.post('/', validate({ body: todoSchema }), (req, res) => {
console.log(req.body);
res.send('Create a new todo item');
});
Code language: JavaScript (javascript)
If you pass an object that doesn’t have a title or completed field with a valid value, it’ll return an error.
For example, if you make the following request:
POST http://localhost:3000/todos
Content-Type: application/json
{
}
Code language: JavaScript (javascript)
… you’ll get a response with 400 HTTP status code and error details:
{
"error": "Invalid data",
"details": {
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"title"
],
"message": "Required"
},
{
"code": "invalid_type",
"expected": "boolean",
"received": "undefined",
"path": [
"completed"
],
"message": "Required"
}
],
"name": "ZodError"
}
}
Code language: JavaScript (javascript)
The reason is that both the title and completed are not available in the request’s body.
Third, validate the route PUT todos/:id
based on both todoIdSchema
and todoSchema
:
router.put(
'/:id',
validate({ params: todoIdSchema, body: todoSchema }),
(req, res) => {
const { id } = req.params;
res.send(`Update the todo item with id ${id}`);
}
);
Code language: JavaScript (javascript)
This ensures that the id is a positive integer and the body contains both title and completed fields with valid values.
Fourth, validate the id of the route DELETE todos/:id
to ensure that the id is a positive integer:
router.delete('/:id', validate({ params: todoIdSchema }), (req, res) => {
const { id } = req.params;
res.send(`Delete the todo item with id ${id}`);
});
Code language: JavaScript (javascript)
Finally, validate the id of the route GET todos/:id
router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
const { id } = req.params;
res.send(`Get the todo item with id ${id}`);
});
Code language: JavaScript (javascript)
Download the project source code
Download the project source code
Summary
- Use the Zod library to validate request data.
- Create a middleware function to make the code more concise.