Validation
Zod schemas plug directly into routes via the request option. The framework parses and validates the request body, query string, and path parameters before the handler runs.
Defining schemas
import { z } from "zod/v4";
const CreateBookSchema = z.object({
title: z.string().trim().min(1).max(200),
authorId: z.uuid(),
genre: z.enum(["fiction", "science-fiction", "fantasy", "non-fiction"]),
description: z.string().max(5000).optional(),
});
const BookQuerySchema = z.object({
genre: z.enum(["fiction", "science-fiction", "fantasy", "non-fiction"]).optional(),
sort: z.enum(["title", "createdAt", "rating"]).default("title"),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
}); Attaching schemas to routes
route.post("/books", {
request: {
body: CreateBookSchema,
},
resolve: (c) => {
if (!c.input.ok) {
return Response.json({ error: c.input.issues }, { status: 400 });
}
// c.input.body is typed as { title: string, authorId: string, genre: string, description?: string }
},
});
route.get("/books", {
request: {
query: BookQuerySchema,
},
resolve: (c) => {
if (!c.input.ok) {
return Response.json({ error: c.input.issues }, { status: 400 });
}
// c.input.query is typed as { genre?: string, sort: string, page: number, limit: number }
},
}); Body validation
The body schema validates the parsed JSON body of POST, PUT, and PATCH requests. The framework calls request.json() and passes the result through the Zod schema.
request: {
body: z.object({
title: z.string().min(1),
tags: z.array(z.string()).max(10).optional(),
}),
}, Query validation
The query schema validates URL query parameters. Parameters are extracted from the URL and passed through the schema.
Use z.coerce.number() for numeric query parameters — query strings are always strings, so coercion converts "1" to 1.
request: {
query: z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
}),
}, Params validation
The params schema validates path parameters. You need a params schema to access path parameters on c.input.params. Without a schema, c.input is not available.
route.get("/books/:id", {
request: {
params: z.object({ id: z.uuid() }),
},
resolve: (c) => {
if (!c.input.ok) {
return Response.json({ error: "Invalid book ID format" }, { status: 400 });
}
const { id } = c.input.params; // validated UUID string
},
}); Checking validation results
Validation is explicit. The framework does NOT automatically reject invalid requests — you check c.input.ok and decide what to do.
resolve: (c) => {
// Always check before using validated data
if (!c.input.ok) {
return Response.json({
error: {
code: "VALIDATION_ERROR",
message: "Invalid input",
issues: c.input.issues,
},
}, { status: 400 });
}
// Safe to use c.input.body, c.input.query, c.input.params
const { title, genre } = c.input.body;
}, Inferring TypeScript types
Use z.infer<> to extract TypeScript types from schemas:
const CreateBookSchema = z.object({
title: z.string(),
genre: z.enum(["fiction", "non-fiction"]),
});
type CreateBook = z.infer<typeof CreateBookSchema>;
// { title: string; genre: "fiction" | "non-fiction" } Common patterns
Shared schemas for create and update
const BookBase = z.object({
title: z.string().min(1).max(200),
genre: z.enum(["fiction", "science-fiction", "fantasy", "non-fiction"]),
description: z.string().max(5000).optional(),
});
const CreateBook = BookBase.extend({ authorId: z.uuid() });
const UpdateBook = BookBase.partial(); // All fields optional Pagination schema
const PaginationQuery = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});