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 Requesterror), saving CPU cycles and preventing unnecessary database hits. - Inferred Typing: Once the request passes the middleware, Zod guarantees at compile-time that
req.bodyperfectly 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