ZZuro Docs

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 validator

Adds validate() middleware so routes can validate body, query, and params with Zod.


What This Generates

src/
└ middleware/
  └ validate.ts
  • src/middleware/validate.ts: exports validate(schema), parses incoming payloads, rewrites req.body/query/params, and returns 422 for 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

  1. Request enters route middleware validate(schema).
  2. Zod runs parseAsync({ body, query, params }).
  3. Valid payload: parsed values are written back to req.
  4. Invalid payload: middleware returns 422 VALIDATION_ERROR.

Configuration

# No required environment variables

validator is fully code-configured through your Zod schemas.


API Reference

  • validate(schema): Express middleware factory for request validation.
  • schema.body: validates and normalizes req.body.
  • schema.query: validates and normalizes req.query.
  • schema.params: validates and normalizes req.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.

On this page