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),
});