Runtime Validation with Zod
Bridge compile-time types and runtime checks — parse unknown input, infer types from schemas, and fail safely at API boundaries.
TypeScript types erase at runtime — tsc cannot stop malformed JSON from crashing your handler. Zod defines schemas that validate at runtime and infer static types from the same definition. One schema serves parse, validate, and type inference — no drift between interface and validator.
Use Zod at trust boundaries: HTTP request bodies, webhook payloads, environment variables, and JSON.parse results. Internal function calls between typed modules rarely need runtime validation — external input always does.
Defining schemas
z.object, z.string, z.number, z.boolean compose like TypeScript shapes. .optional(), .nullable(), and .default() mirror optional fields. z.enum replaces string literal unions with runtime membership checks.
parse vs safeParse
parse throws ZodError on failure — fine when invalid input is exceptional. safeParse returns { success: true, data } | { success: false, error } — preferred for HTTP handlers returning 400 with field errors instead of 500 stack traces.
{ email: ['Invalid email'], id: ['Invalid uuid'] }Transform and refine
z.coerce.number converts string query params to numbers. .refine adds custom validation — password confirmation matching, cross-field rules. .transform maps validated input to domain types after parsing.
API boundary pattern
Handlers receive unknown, safeParse once, return 400 on failure, proceed with typed data on success. Share schemas in packages/shared so frontend and backend validate the same shape — optional but powerful for form and API parity.
| Method | On failure |
|---|---|
| parse | throws ZodError |
| safeParse | returns success: false |
| parseAsync | async throw |
| safeParseAsync | async Result |
- Zod validates what types cannot at runtime
- z.infer keeps types synced
- safeParse for user-facing errors
- 1Schema defines runtime + static shape
- 2safeParse at HTTP boundaries
- 3Share schemas in monorepos
- 4Next: migrating JavaScript to TypeScript