Zod Validation in Middlewares

Express & Next.js

Zod is a TypeScript-first schema declaration and validation library. One of the best practices in the Node.js ecosystem (Express, Next.js, etc.) is to use Zod within a middleware to validate incoming requests before they ever reach your main Controller logic.

Why use it in a middleware?

  • Fail-Fast: Malformed requests are rejected immediately (with a 400 Bad Request error), saving CPU cycles and preventing unnecessary database hits.
  • Inferred Typing: Once the request passes the middleware, Zod guarantees at compile-time that req.body perfectly matches your expected type.
  • Clean Code: It removes repetitive manual checks (if (!req.body.name) return res...) from inside your business logic routes.

Example: Generic Express Middleware

Create a middleware “factory” that receives a generic Zod schema and performs the validation:

import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";

export const validate =
  (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => {
    try {
      // Parses body, query, or params and throws an error if invalid
      schema.parse({
        body: req.body,
        query: req.query,
        params: req.params,
      });
      next(); // Success! Move to the next handler.
    } catch (error) {
      if (error instanceof ZodError) {
        return res.status(400).json({
          message: "Validation Error",
          errors: error.errors,
        });
      }
      next(error); // Pass generic errors forward
    }
  };

How to plug it into a route:

import { z } from "zod";

// Define the expected schema
const userSchema = z.object({
  body: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
});

// The route logic only runs if validate(userSchema) passes
app.post("/users", validate(userSchema), (req, res) => {
  // From here on, req.body is guaranteed to be valid!
  res.status(201).send("User created");
});

Example: Validation in Next.js (App Router / Route Handlers)

Although Next.js doesn’t have the same “route middleware” chain as Express, you can encapsulate the logic natively in Route Handlers:

import { NextResponse } from "next/server";
import { z } from "zod";

const bodySchema = z.object({
  id: z.string(),
});

export async function POST(req: Request) {
  try {
    const body = await req.json();

    // Manual parse. If it fails, execution drops to the catch block
    const data = bodySchema.parse(body);

    return NextResponse.json({ success: true, data });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ errors: error.errors }, { status: 400 });
    }
    return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
  }
}

Related: express-middleware-chain · express-fluxo-de-trabalho

Built with Eleventy · search by Lunr.js