Skip to main content

Zod Validation

BurgerAPI uses Zod to check that incoming requests are shaped the way you expect. A schema is just a description of the data you want (for example, "the query string must have a limit that is a number"). You write these schemas for query parameters, path parameters, request headers, cookies, and request bodies. BurgerAPI checks the request before your handler runs and puts the clean, typed result on ctx.validated.

BurgerAPI uses Zod 4.x, which gives clear, friendly error messages.

Zod is the default schema provider, but it is not the only one. Any library that follows the Standard Schema contract (such as Valibot or ArkType) also works. See Standard Schema Support.

A first example​

api/products/schema.ts
import { z } from "zod";

export const GET = {
query: z.object({
limit: z.coerce.number().int().min(1).max(100).optional(),
}),
};
api/products/route.ts
import { defineRoute } from "burger-api";
import { GET as GetSchema } from "./schema";

export const GET = defineRoute(GetSchema, (ctx) => {
const { limit } = ctx.validated.query;
return Response.json({ limit });
});

Here z.coerce.number() turns the text "50" into the number 50 before the checks run. BurgerAPI also offers built-in automatic type conversion you can turn on for a whole app. See Coercion.

See Schema Definition for the full shape of a validation schema and Validation for the big picture.

Types for this feature​

Your Zod schema is both a runtime check and a type. The handler that uses it gets the inferred type for free.

The types you use (from burger-api):

  • defineRoute(schema, handler): infers ctx.validated from schema; no generic to write
  • BurgerContext<typeof GET>: the same inference, written by hand
  • z.infer<typeof GET.query>: the type of one slot, for variables or helpers

✅ Correct. defineRoute infers the schema type automatically:

api/products/route.ts
import { defineRoute } from "burger-api";
import { GET as GetSchema } from "./schema";

export const GET = defineRoute(GetSchema, (ctx) => {
const { limit } = ctx.validated.query; // typed: number | undefined
return Response.json({ limit });
});

✅ Correct. Reuse a slot's type elsewhere:

import { z } from "zod";
import type { GET } from "./schema";
type Query = z.infer<typeof GET.query>; // { limit?: number }
function usesLimit(limit: Query["limit"]) { /* ... */ }

❌ Wrong. An unannoted handler has no validated types:

export async function GET(ctx: BurgerContext) {
ctx.validated.query.limit; // ❌ 'validated' is possibly undefined and 'query' is unknown
}

See the TypeScript overview.

Check your code: bun run typecheck.