Recipe
Zod schema patterns
Composable runtime validation that stays in sync with your TypeScript types.
Why Zod
Zod gives you a single source of truth for both TypeScript types and runtime validation. Parse once at the boundary — API handlers, form actions, environment config — and the rest of your code trusts the shape.
Basic shape
import { z } from "zod"
const UserSchema = z.object({
email: z.string().email(),
age: z.number().min(13).max(120),
plan: z.enum(["free", "pro", "enterprise"]),
})
type User = z.infer<typeof UserSchema>Refinement
const PasswordSchema = z
.string()
.min(8)
.refine((pw) => /[A-Z]/.test(pw), {
message: "Need one uppercase letter",
})Composition
const Base = z.object({ id: z.string().uuid() })
const WithMeta = Base.extend({ createdAt: z.date() })
const ApiResponse = z.discriminatedUnion("ok", [
z.object({ ok: z.literal(true), data: WithMeta }),
z.object({ ok: z.literal(false), error: z.string() }),
])Server action guard
const FormSchema = z.object({
title: z.string().min(1).max(200),
tags: z.array(z.string()).max(5),
})
export async function submit(raw: FormData) {
const parsed = FormSchema.safeParse(
Object.fromEntries(raw)
)
if (!parsed.success) return { errors: parsed.error.flatten() }
// parsed.data is fully typed
}Takeaway
- 1.Define schemas at the I/O boundary — never deep in business logic.
- 2.Prefer
safeParseoverparsein server actions so you control the error shape. - 3.Use
z.inferinstead of hand-writing types — single source of truth.