Back to docs
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 safeParse over parse in server actions so you control the error shape.
  • 3.Use z.infer instead of hand-writing types — single source of truth.