Request API
The ctx object your handlers receive exposes request data through a small, consistent set of properties. This page shows how to use each one.
ctx is a BurgerContext. It is not a Request subclass: the original request is available as ctx.request, and the standard request surface is delegated.
The examples on this page assume import type { BurgerContext } from "burger-api";.
ctx.request
The raw Request. Use it when you need the full request object:
export async function GET(ctx: BurgerContext) {
const url = new URL(ctx.request.url);
return Response.json({ path: url.pathname });
}
ctx.params
Dynamic path parameters:
export async function GET(ctx: BurgerContext) {
const { id } = ctx.params;
return Response.json({ id });
}
When to use: any time the route has a [param] segment. Wildcard segments are available as ctx.wildcardParams.
ctx.query
The parsed query string, available lazily:
export async function GET(ctx: BurgerContext) {
const { limit = "10", tag } = ctx.query;
return Response.json({ limit, tag });
}
Why: ctx.query parses only when you read it, so requests that don't use the query pay nothing for parsing. See Query Parsing.
ctx.cookies
Parsed cookies, also lazy. The record maps cookie names to values.
ctx.body
The raw request body stream (ReadableStream | null), delegated from ctx.request.
ctx.json() and ctx.text()
Read the request body as JSON or text. The body is read once and cached on ctx, so ctx.json(), ctx.text() and ctx.arrayBuffer() can be called after validation read the stream:
export async function POST(ctx: BurgerContext) {
const body = await ctx.json();
return Response.json({ received: body });
}
When the route declares a body schema, read the validated body from ctx.validated.body instead. It is typed from the schema and non-optional after validation. Calling ctx.json() still works (the parsed body is cached), but the validated value is the one to use. A declared body schema also means non-JSON requests are rejected with 415 Unsupported Media Type before the handler runs.
ctx.headers, ctx.method, ctx.url
The request headers, HTTP method, and full URL. All delegate to the underlying request and never allocate anything extra:
export async function GET(ctx: BurgerContext) {
const token = ctx.headers.get("authorization");
return Response.json({ method: ctx.method });
}
ctx.route
The matched route's identity:
export async function GET(ctx: BurgerContext) {
// ctx.route?.pattern === "/api/users/:id" (includes apiPrefix)
return Response.json({ pattern: ctx.route?.pattern });
}
The route is always set in a matched handler, but the type is RouteMeta | undefined, so use optional chaining. See Route Metadata.
ctx.validated
Data that passed your schemas, typed from schema.ts:
import { z } from "zod";
export const GET = { query: z.object({ limit: z.coerce.number().optional() }) };
import { defineRoute } from "burger-api";
import { GET as GetSchema } from "./schema";
export const GET = defineRoute(GetSchema, (ctx) => {
const { limit } = ctx.validated.query; // typed + validated
return Response.json({ limit });
});
Why: validated data is typed from your schema, so handlers know exactly what they received without manual casts. See Validation.
ctx.services
Application services registered in src/providers.ts, typed via module augmentation. The services object is app-scoped, shared by every request, and frozen. Read from it, never assign to it; per-request data belongs in a transform hook instead. See Configuration.
ctx.ip
The client socket address, resolved lazily on first read. It is never read from forwarded headers such as X-Forwarded-For: those are client-controlled. On Bun, serve() reads it from the server socket; @burger-api/node-server sets it from the Node socket; on plain toFetchHandler runtimes it is undefined. The Rate Limiter hook uses it as the default client key.
ctx.set
Response mutations, covered in Response Mutation.
Types for this feature
The type you use: BurgerContext, or BurgerContext<typeof GET> when the route has a schema.
✅ Correct: type the handler parameter:
import type { BurgerContext } from "burger-api";
export async function GET(ctx: BurgerContext) {
const { limit = "10" } = ctx.query;
return Response.json({ limit });
}
✅ Correct: give the JSON body a type with ctx.json<T>():
export async function POST(ctx: BurgerContext) {
const body = await ctx.json<{ name: string }>();
return Response.json({ name: body.name });
}
❌ Wrong: untyped handler parameters:
export async function GET(ctx) {
// ❌ Parameter 'ctx' implicitly has an 'any' type
}
Unannoted BurgerContext keeps every request field (query, params, cookies) untyped. For typed request data, add a schema and wrap the handler with defineRoute(schema, handler) (or use BurgerContext<typeof GET> directly). See Validation and the TypeScript overview.
Check your code: bun run typecheck.