Zod 44,027 (github.com/colinhacks/zod (https://github.com/colinhacks/zod 44,027 ), npm 2,036 i zod, 4.6.5 at the time of writing) is a schema library in which the schema is the type: you describe the accepted shape once, Zod checks values against it at runtime, and in TypeScript it infers the static type from the same object, so the two cannot drift apart. It has zero runtime dependencies. schema.parse(value) returns the parsed value or throws a ZodError; schema.safeParse(value) returns { success, data } or { success, error } and never throws. Middleware wants safeParse: a failed validation is an expected outcome, not an exception.
import { z } from 'zod';
export const bookBody = z.object({
title: z.string().min(1).max(200),
author: z.string().min(1),
year: z.number().int().min(1450).max(2026),
isbn: z.string().regex(/^\d{13}$/, 'ISBN must be 13 digits'),
tags: z.array(z.string()).max(5).default([])
});
const result = bookBody.safeParse({ title: '', author: 'Kernighan', year: 2100, isbn: '978' });
console.log('success:', result.success, '- issues:', result.error.issues.length);
console.log(JSON.stringify(result.error.issues[2]));success: false - issues: 3
{"origin":"string","code":"invalid_format","format":"regex","pattern":"/^\\d{13}$/",
"path":["isbn"],"message":"ISBN must be 13 digits"}All three problems are reported at once, not just the first. Each issue carries a machine-readable code, a path into the value, a human message, and extras specific to the code. The path lets a client highlight the right form field; the code lets it translate the message. z.flattenError(error) reshapes the issues into { formErrors, fieldErrors } keyed by field name, and z.treeifyError nests them for deep objects.
Two behaviors matter more than they look. z.coerce.number() turns the "20" that a query string always delivers into a number before the range checks run — a listQuery schema of { limit: z.coerce.number().int().min(1).max(100).default(20), offset: ... } is all a paginated route needs. And a plain z.object() strips keys it does not know about, so { title: 'Go', role: 'admin' } parses to { title: 'Go' }: mass assignment closed by default. z.strictObject() makes an unexpected key an error instead, reporting code: 'unrecognized_keys'.