Skip to main content

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:

api/orders/schema.ts
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.