Validation Best Practices
A few habits that keep validation clear, fast, and easy to maintain.
Start simple
Begin with plain Zod schemas on the slots you actually use. You do not need every feature on day one:
import { z } from "zod";
export const POST = {
body: z.object({ name: z.string().min(1), price: z.number() }),
};
Use coercion for text inputs
Query strings, params, headers, and cookies are always text. If you expect numbers or booleans there, turn on coercion (coerce: true) instead of hand-writing z.coerce.* on every field.
Share shapes across routes
When the same shape appears in more than one route, define it once in a schemas file and import it. See Schema Definition. One source of truth, still fully typed.
Keep handlers thin
Validation runs before your handler, so the handler can trust ctx.validated. Do not re-check types inside the handler; just use the data.
Add response validation where it matters
Use response schemas in dev mode to get free feedback that a handler returns the wrong shape. Switch specific routes to enforce only when you want production to reject mismatches; an enforced mismatch answers a generic application/problem+json 500 in production, while afterRoute and mapResponse still run. Set the app-wide mode with validation.responseValidation, and override it per route in that route's config.ts.
Choose the right error format
The default error body follows RFC 9457. If your clients expect the simpler grouped body, set errorFormat: "plain". See Problem Details. For full control, supply an errorRenderer.
Don't over-validate
Validate what your handler depends on. Validating fields you never read adds no value and only costs a little time.