TypeScript Types
BurgerAPI is built with TypeScript. TypeScript checks your code before it runs. Wrong code fails before your server starts.
This page is the starting point for types. Each feature page has a short "Types for this feature" section at the bottom. This page explains the shared ideas behind all of them.
The one pattern to remember
Most typing comes from one idea: the handler knows its own schema.
The recommended way to wire that up is defineRoute(schema, handler). Pass the schema you already import from schema.ts as the first argument, and ctx is inferred from it, with no generic to type by hand:
// schema.ts
import { z } from "zod";
export const GET = { query: z.object({ q: z.string().optional() }) };
// route.ts
import { defineRoute } from "burger-api";
import { GET as GetSchema } from "./schema";
export const GET = defineRoute(GetSchema, (ctx) => {
ctx.validated.query; // { q?: string }, typed and non-optional
return Response.json(ctx.validated.query);
});
defineRoute does nothing at runtime; it returns your handler exactly as written. What it buys you is that the schema you pass and the schema TypeScript types ctx against are always the literal same object, so the two can never quietly drift apart. Runtime validation still comes from the route's schema.ts convention file, so always pass that same export rather than an inline schema object.
The equivalent, older form still works: annotate the handler with BurgerContext<typeof GET> directly:
import type { BurgerContext } from "burger-api";
import type { GET as RouteSchema } from "./schema";
export async function GET(ctx: BurgerContext<typeof RouteSchema>) {
ctx.validated.query; // same inference, written by hand
}
Use defineRoute for new code; the manual-generic form is documented here because it's what defineRoute is built on, and existing code using it keeps working unchanged.
For a route without a schema, keep plain BurgerContext (or call defineRoute with an empty {} schema). Every slot stays unknown.
Typing hooks.ts the same way
hooks.ts hooks see a plain BurgerContext by default, since hooks aren't tied to one method's schema the way a handler is. When a route's hooks do read ctx.validated, wrap them with defineHooks(schema, hooks) the same way:
// hooks.ts
import { defineHooks } from "burger-api";
import { GET as GetSchema } from "./schema";
export const { beforeRoute } = defineHooks(GetSchema, {
beforeRoute: (ctx) => {
ctx.validated.query; // typed; slots are optional because hooks run for every method
},
});
Words we use
- Type: the shape of a value. Example:
stringis the type of text. - Compile time: when TypeScript checks your code, before it runs.
- Runtime: when your code actually runs.
- Inference: TypeScript figures out a type by itself, without you writing it.
- Generic: a type that takes another type as input. Example:
BurgerContext<typeof GET>uses the schema typetypeof GET. - Augmentation: you add your own types to a BurgerAPI type. See below.
- Compile error: TypeScript says your code is wrong. The server does not start.
How to check your code
In your project folder, run:
bun run typecheck
No errors means your types are correct. Your editor shows the same problems while you type (red squiggly lines).
The types BurgerAPI gives you
| Type | What it does | Where you use it |
|---|---|---|
BurgerContext | The request object passed to handlers and hooks | Handler and hook parameters |
BurgerContext<typeof GET> | The request object, with ctx.validated typed from your schema | Route handlers |
defineRoute(schema, handler) | Infers ctx from schema, no generic to write by hand | Route handlers (recommended) |
defineHooks(schema, hooks) | The same inference for a route's hooks.ts | Route hooks that read ctx.validated |
RequestHandler | A handler function: takes BurgerContext, returns Response | Typing handler variables |
HTTPMethod | The allowed methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS | Typing method keys |
RouteDefinition | A route with handlers, schema, and openapi metadata | Programmatic apiRoutes |
RouteSchema | The shape of a route's full schema map | Programmatic schema |
MethodSchema | The shape of one schema.ts method export | satisfies MethodSchema in schema.ts |
OpenAPIMeta | The shape of one openapi.ts method export | satisfies OpenAPIMeta in openapi.ts |
RouteConfig | The route config.ts options; you augment it to type ctx.config | declare module "burger-api" |
BuildConfig | The shape of burger.build.ts | satisfies Partial<BuildConfig> in burger.build.ts |
OpenAPIConfig | The shape of openapi.config.ts | satisfies OpenAPIConfig |
ForwardHook | A before-handler hook: returns Response, a mapper, or undefined | onRequest, beforeRoute |
ResponseHook | An after-handler hook: returns Response, a mapper, or undefined | afterRoute, mapResponse |
ErrorHook | An error hook: takes (error, ctx), returns Response or undefined | onError |
RouteHooks | All hook points of a route in one object (no onRequest) | hooks.ts files |
GlobalHooks | RouteHooks plus onRequest | src/hooks.ts, plugin hooks |
Plugin | A plugin: name plus optional hooks | usePlugin() |
BurgerServices | The services on ctx.services, which you extend | Providers, ctx.services |
WebSocketData | The data on ws.data, which you extend | WebSocket handlers |
BurgerWS | The WebSocket object passed to WS handlers | WS handlers |
openapi | OpenAPI metadata, keyed by lowercase method | Programmatic openapi |
Convention files and their types
Every convention file has a consumer type:
| File | Type | Example |
|---|---|---|
burger.build.ts | BuildConfig | export default {...} satisfies Partial<BuildConfig>; |
src/hooks.ts | GlobalHooks | export const beforeRoute: GlobalHooks["beforeRoute"] = []; |
src/index.ts | ServerOptions | typed by new Burger({...}) |
openapi.config.ts | OpenAPIConfig | export default {...} satisfies OpenAPIConfig; |
route schema.ts | MethodSchema | export const GET = {...} satisfies MethodSchema; |
route openapi.ts | OpenAPIMeta | export const GET = {...} satisfies OpenAPIMeta; |
route hooks.ts | RouteHooks | export const beforeRoute: RouteHooks["beforeRoute"] = []; |
route config.ts | RouteConfig | augment RouteConfig, then export default {...} |
route route.ts | defineRoute(schema, handler) | import { GET as GetSchema } from "./schema"; export const GET = defineRoute(GetSchema, (ctx) => ...) |
ws ws.ts | BurgerWS, WebSocketHandlers | typed handler parameters |
src/types.ts | app type extensions | declare module 'burger-api' { ... } |
JavaScript projects get the same checks through JSDoc hints (/** @type {Partial<import('burger-api').BuildConfig>} */).
Augmentation: add your own types
Some BurgerAPI types start empty. You fill them with your own types. This is called augmentation. Put this block in src/types.ts (the single home for app-wide type extensions). burger-api create scaffolds that file for TypeScript projects with a commented example, so the home already exists:
import type { Database, Logger, User } from "./my-types";
declare module "burger-api" {
// Types for ctx.services (from burger.provide)
interface BurgerServices {
db: Database;
logger: Logger;
}
// Custom properties you set in transform hooks
interface BurgerContext {
tenant: string;
}
// Fields of the authenticated user, shared by ctx.user and ws.user.
// Auth plugins merge their fields here too.
interface BurgerAuthUser {
userId?: string;
}
// Types for ws.data in WebSocket handlers
interface WebSocketData {
room?: string;
}
}
After this block, ctx.services.db, ctx.tenant, ctx.user?.userId, and ws.data.room are all typed. Augment BurgerAuthUser (not BurgerContext.user) for user fields: that keeps every auth plugin and your own types on one shape.
RouteConfig ships with responseValidation typed ("off" | "dev" | "enforce"); augment it for your own route options:
declare module "burger-api" {
interface RouteConfig {
auth?: boolean | { required?: boolean; roles?: string[] };
cache?: number;
}
}
What is NOT typed
These stay untyped on purpose:
ctx.params: the raw URL parameters, alwaysRecord<string, string>(empty when the route has none). For typed parameters, add aparamsschema and usectx.validated.params. See Dynamic Routes.ctx.wildcardParams: alwaysstring[](empty for non-wildcard routes). There is no schema for wildcard segments.ctx.json(): the default isany(the same as the browserRequest). Usectx.json<T>()to give it a type:await ctx.json<{ id: number }>().
Where types live in each feature
- Routing: typed handlers and parameters
- Hooks: typed hook contracts
- Request handling:
BurgerContextand body reading - Validation: typed
ctx.validated - Configuration:
ServerOptionsand route definitions - Plugins:
Pluginand services - OpenAPI: typed metadata
- WebSocket:
BurgerWSandws.data