Schema Definition
The validation schema lives in schema.ts, one per route directory. It exports one object per HTTP method (uppercase: GET, POST, and so on). For each method you describe the data you expect.
In short: a schema is a small description of "what the request should look like". BurgerAPI reads it, checks the real request against it, and gives you clean data to work with.
The full shape
import { z } from "zod";
export const GET = {
// path parameters, e.g. /users/:id
params: z.object({ id: z.string() }),
// query string, e.g. ?limit=10 (query values arrive as strings)
query: z.object({ limit: z.coerce.number() }),
// request headers
headers: z.object({ "x-api-key": z.string() }),
// cookies
cookies: z.object({ session: z.string() }),
// JSON request body (POST/PUT)
body: z.object({ name: z.string() }),
// per-route opt-in for automatic type conversion
coerce: true,
};
export const POST = {
body: z.object({ name: z.string() }),
// validate what the handler returns
response: { 200: z.object({ id: z.string() }) },
};
Every slot is optional. You only describe what you actually use.
Reusing a schema across routes
When the same shape appears in several routes, define it once and import it. Plain TypeScript, no registry to learn:
import { z } from "zod";
export const Pagination = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
import { Pagination } from "../../schemas";
export const GET = { query: Pagination }; // shared, fully typed
Validated data
After validation, the result is available on ctx.validated:
ctx.validated.paramsctx.validated.queryctx.validated.headersctx.validated.cookiesctx.validated.body
Each is typed from the corresponding schema. Wrap the handler with defineRoute(schema, handler) to get the inferred types automatically, or annotate it with BurgerContext<typeof GET> by hand. See Zod Validation, Query, and Body.
Types for this feature
The schema is the source of your types. The handler that uses it gets those types for free.
The types you use (from burger-api):
defineRoute(schema, handler): infers the handler type fromschema; no generic to writeBurgerContext<typeof GET>: the same inference, written by hand.typeof GETis the schema export.ctx.validated: the validated data, typed slot by slot.RouteSchema: a route's full schema map (for programmatic routes).
✅ Correct. Wrap the handler with defineRoute:
import { defineRoute } from "burger-api";
import { GET as GetSchema } from "./schema";
export const GET = defineRoute(GetSchema, (ctx) => {
const { id } = ctx.validated.params; // typed: string
const { limit } = ctx.validated.query; // typed: number
return Response.json({ id, limit });
});
❌ Wrong. Using a slot you did not declare:
export const GET = defineRoute(GetSchema, (ctx) => {
ctx.validated.body.name; // ❌ 'body' is unknown (no body schema on GET)
});
A slot without a schema is unknown and is never set at runtime. See the TypeScript overview.
Check your code: bun run typecheck.