Published on January 14, 2025

How to use Zod with Express for input validation?

How to use Zod with Express for input validation?

When building web applications, a common challenge is making sure the data you receive matches what you expect. This protects against errors caused by unexpected user input and helps prevent security problems that can come from invalid data. There’s a popular quote that I think is a good rule to keep in mind when building software products:

All user input is error. — Elon Musk

Thankfully, there are libraries like Zod that make it easier to validate data in TypeScript. I use it across my projects, both on the front end and back end. In this post, I’ll show you how to use Zod with Express to create a validation tool for your application.

Setting up Your Project

First, let’s install the necessary packages. For this example, we need to install Zod, Express, and the TypeScript types for Express and Node.js. I’ll be using ts-node to run the TypeScript code directly, but of course you can use any other method you prefer.

npm install express zod
npm install -D @types/express @types/node ts-node typescript

Diving into the Code

First, we import the necessary modules:

import { Request, Response, NextFunction } from 'express';
import express from 'express';
import { z } from 'zod';
  • Request, Response, and NextFunction are types from Express that are needed for handling HTTP requests.
  • express is the Express framework itself.
  • z is the main object from the Zod library, used for creating validation schemas.

Defining the Middleware Type

Next, we define a type for our middleware function:

type MiddlewareFunction = (
  req: Request,
  res: Response,
  next: NextFunction
) => void;

This type makes it clear what our middleware function should look like. It takes a request, a response, and a next function as arguments, and it does not return anything.

Creating the Validation Middleware

The core of our validation logic is in the validateInput function:

type ValidateInput = (
  schema: z.ZodObject<{
    body?: z.ZodTypeAny;
    query?: z.ZodTypeAny;
    params?: z.ZodTypeAny;
  }>
) => MiddlewareFunction;

const validateInput: ValidateInput =
  (schema): MiddlewareFunction =>
  (req, res, next) => {
    // Validate the input
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params
    });

    // If the validation fails, return a 400 status with the validation errors
    if (!result.success) {
      return res.status(400).json({
        status: 'error',
        message: 'Validation failed',
        errors: result.error.errors.map((err) => ({
          path: err.path.join('.'),
          message: err.message,
          code: err.code
        }))
      });
    }

    // Call the next middleware or route handler
    next();
  };

This function is a higher-order function, meaning it returns another function. It takes a Zod schema as an argument and returns a middleware function.

  • The middleware function uses schema.safeParse to validate the request body, query parameters, and route parameters against the provided schema.
  • If the validation fails, it sends a 400 status code with a JSON response that includes the validation errors.
  • If the validation passes, it calls next() to pass control to the next middleware or route handler.

Setting up Express

Now, we set up the Express application:

const app = express();
app.use(express.json());
  • We create an Express application using express().
  • app.use(express.json()) adds middleware that parses incoming requests with JSON payloads.

Defining the Zod Schema

We define a Zod schema for our user data:

const userSchema = z.object({
  body: z.object({
    name: z.string().min(2),
    email: z.string().email(),
    age: z.number().int().positive().optional()
  })
});
  • This schema defines the expected structure for the request.
  • It expects a body object with a name string that is at least 2 characters long, an email string that is a valid email, and an optional age number that must be a positive integer.

Using the Middleware in a Route

We use the validateInput middleware in our route:

app.post('/users', validateInput(userSchema), (req, res) => {
  const { name, email, age } = req.body;
  res.status(201).json({ message: 'User created successfully' });
});
  • The validateInput(userSchema) middleware is added to the /users POST route.
  • If the validation passes, the route handler is executed, which sends a 201 status code with a success message.

Starting the Server

Finally, we start the server:

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

The server listens on port 3000, and a message is logged to the console when the server starts.

Run the Code

  1. Save the code in a file named index.ts.

  2. Run the following command in your terminal:

    npx ts-node index.ts
    

Make a Request

Now that the server is running, you can test it by sending a POST request to http://localhost:3000/users. Here’s an example using cURL:

curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "John Doe", "email": "john.doe@example.com", "age": 25}'

Once we make a request to the server, the middleware will validate the input and once the input is valid, the route handler will be executed.

Final Thoughts

This is a very simple example, but it shows how easy it is to use Zod with Express to create an input validation middleware. Of course, you can go a lot further by improving the error handling in the middleware and passing the parsed data to the route handler.