Node.js cookbook · Beginner
Validate Express requests with Zod
Reject invalid bodies and query strings with clear 422 errors and get typed data in handlers.
Approach
Write a small middleware factory that parses req.body/req.query/req.params with Zod schemas, returns a Problem Details 422 with field errors on failure, and stores the parsed (coerced, typed) values for the handler.
Solution
import type { NextFunction, Request, Response } from "express";
import { z, type ZodType } from "zod";
export function validate<B extends ZodType, Q extends ZodType>(schemas: { body?: B; query?: Q }) {
return (req: Request, res: Response, next: NextFunction) => {
const errors: { field: string; message: string }[] = [];
for (const [part, schema] of Object.entries(schemas) as [keyof typeof schemas, ZodType][]) {
const result = schema.safeParse(req[part]);
if (!result.success) {
errors.push(...result.error.issues.map((i) => ({ field: [part, ...i.path].join("."), message: i.message })));
} else {
res.locals[part] = result.data; // typed, coerced data
}
}
if (errors.length) {
return res.status(422).type("application/problem+json").json({ title: "Validation failed", status: 422, errors });
}
next();
};
}
const CreateOrder = z.object({
items: z.array(z.object({ sku: z.string().min(1), qty: z.number().int().positive() })).min(1).max(50),
couponCode: z.string().trim().toUpperCase().optional(),
});
const ListOrders = z.object({ limit: z.coerce.number().int().min(1).max(100).default(20), cursor: z.string().optional() });
app.post("/api/orders", requireAuth, validate({ body: CreateOrder }), async (req, res) => {
const body = res.locals.body as z.infer<typeof CreateOrder>;
res.status(201).json(await orders.create(req.user!.id, body));
});
app.get("/api/orders", requireAuth, validate({ query: ListOrders }), async (req, res) => { /* res.locals.query */ });
Pitfalls
- Trusting
req.bodytypes without runtime validation. - Forgetting
express.json({ limit })body size limits. - Returning only the first error.
Variations
- Fastify uses JSON Schema natively (or
fastify-type-provider-zod). - Generate OpenAPI from Zod with
zod-openapi.
Further reading: Zod documentation
Reference: https://zod.dev/
More Node.js recipes
- Add health and readiness endpoints to a Node.js service
- Add JWT authentication to an Express API
- Cache expensive lookups in Redis from Node.js
- Call external APIs with timeouts and retries in Node.js
- Centralize error handling in Express 5
- Hash passwords and implement login safely in Node.js
- Implement cursor pagination with PostgreSQL in Node.js
- Offload CPU-heavy work with worker threads