Validator
Published Oct 6, 2025 | Updated Mar 13, 2026
Add request validation middleware powered by Zod
Overview
Add production-ready request validation to your backend with a single middleware.
Install
npx zuro add validatorAdds validate() middleware so routes can validate body, query, and params with Zod.
What This Generates
src/
└ middleware/
└ validate.tssrc/middleware/validate.ts: exportsvalidate(schema), parses incoming payloads, rewritesreq.body/query/params, and returns422for invalid input.
Quick Example
import { Router } from "express";
import { z } from "zod";
import { validate } from "../middleware/validate";
const router = Router();
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
});
router.post("/users", validate(createUserSchema), async (req, res) => {
res.status(201).json({
success: true,
user: req.body,
});
});Example request:
POST /users
Content-Type: application/json
{
"name": "Ava",
"email": "not-an-email"
}Example response:
{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"errors": [
{
"path": "body.email",
"message": "Invalid email"
}
]
}How It Works
- Request enters route middleware
validate(schema). - Zod runs
parseAsync({ body, query, params }). - Valid payload: parsed values are written back to
req. - Invalid payload: middleware returns
422 VALIDATION_ERROR.
Configuration
# No required environment variablesvalidator is fully code-configured through your Zod schemas.
API Reference
validate(schema): Express middleware factory for request validation.schema.body: validates and normalizesreq.body.schema.query: validates and normalizesreq.query.schema.params: validates and normalizesreq.params.422 VALIDATION_ERROR: automatic error response with{ path, message }[]issues.
Advanced Usage
Validate query + params with coercion:
const listSchema = z.object({
params: z.object({
projectId: z.string().uuid(),
}),
query: z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
}),
});
router.get("/projects/:projectId/issues", validate(listSchema), (req, res) => {
res.json({
projectId: req.params.projectId,
page: req.query.page,
limit: req.query.limit,
});
});Reuse shared schema blocks:
const pagination = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().default(20),
});
const schema = z.object({
query: pagination,
});Example Use Cases
- User signup/login payload validation.
- Webhook payload shape enforcement.
- Search/filter query validation with type coercion.
- ID/slug param validation for REST routes.
Combining body, query, and params
A single schema can validate all three parts of a request at once:
const updatePostSchema = z.object({
params: z.object({
id: z.string().uuid("Post ID must be a valid UUID"),
}),
query: z.object({
notify: z.enum(["true", "false"]).optional(),
}),
body: z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
published: z.boolean().optional().default(false),
}),
});
router.put("/posts/:id", validate(updatePostSchema), asyncHandler(async (req, res) => {
// req.params.id — validated UUID string
// req.query.notify — "true" | "false" | undefined
// req.body.title, .content, .published — validated and typed
res.json({ updated: true });
}));Reusable Schema Blocks
Define shared schema fragments once and compose them:
// src/lib/schemas/common.ts
import { z } from "zod";
export const pagination = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
export const uuidParam = z.object({
id: z.string().uuid(),
});Use them in route schemas:
import { pagination, uuidParam } from "../lib/schemas/common";
const listPostsSchema = z.object({
params: uuidParam,
query: pagination.extend({
status: z.enum(["draft", "published"]).optional(),
}),
});TypeScript Type Inference
The validate middleware rewrites req.body, req.query, and req.params with parsed values, but TypeScript still needs a type cast to use them safely in your handler:
type CreateUserBody = z.infer<typeof createUserSchema>["body"];
router.post("/users", validate(createUserSchema), asyncHandler(async (req, res) => {
const { email, name } = req.body as CreateUserBody;
res.status(201).json({ email, name });
}));Troubleshooting
422 returned even though the data looks correct
Zod schemas are strict by default on .object(). Extra fields not defined in the schema cause no error (Zod strips them), but missing required fields do. Use .partial() for optional fields and .passthrough() if you want to allow extra keys.
z.coerce.number() returning NaN
Query string values are always strings. Use z.coerce.number() to convert them. If the value is empty string (""), coercion produces NaN — add .min(1) or make the field optional:
z.coerce.number().int().positive().optional()Validation passes but req.body is still the raw unvalidated value
Ensure express.json() middleware is registered in app.ts before your routes. Without it, req.body is undefined and Zod validates undefined against your schema.
Custom error messages not appearing in responses Pass a message to the Zod schema method:
z.string().email("Please provide a valid email address")
z.string().min(8, "Password must be at least 8 characters")The message appears in the errors[].message field of the 422 response.