Mastering Express Validation with Zod

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 zodCode 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/abcCode language: JavaScript (javascript)

… you’ll get the following response:

Get the todo item with id abcCode 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/abcCode 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/0Code 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/1Code 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 middlewareCode 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 schemasCode 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.
Was this tutorial helpful ?