Skip to main content

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: string is 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 type typeof 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​

TypeWhat it doesWhere you use it
BurgerContextThe request object passed to handlers and hooksHandler and hook parameters
BurgerContext<typeof GET>The request object, with ctx.validated typed from your schemaRoute handlers
defineRoute(schema, handler)Infers ctx from schema, no generic to write by handRoute handlers (recommended)
defineHooks(schema, hooks)The same inference for a route's hooks.tsRoute hooks that read ctx.validated
RequestHandlerA handler function: takes BurgerContext, returns ResponseTyping handler variables
HTTPMethodThe allowed methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONSTyping method keys
RouteDefinitionA route with handlers, schema, and openapi metadataProgrammatic apiRoutes
RouteSchemaThe shape of a route's full schema mapProgrammatic schema
MethodSchemaThe shape of one schema.ts method exportsatisfies MethodSchema in schema.ts
OpenAPIMetaThe shape of one openapi.ts method exportsatisfies OpenAPIMeta in openapi.ts
RouteConfigThe route config.ts options; you augment it to type ctx.configdeclare module "burger-api"
BuildConfigThe shape of burger.build.tssatisfies Partial<BuildConfig> in burger.build.ts
OpenAPIConfigThe shape of openapi.config.tssatisfies OpenAPIConfig
ForwardHookA before-handler hook: returns Response, a mapper, or undefinedonRequest, beforeRoute
ResponseHookAn after-handler hook: returns Response, a mapper, or undefinedafterRoute, mapResponse
ErrorHookAn error hook: takes (error, ctx), returns Response or undefinedonError
RouteHooksAll hook points of a route in one object (no onRequest)hooks.ts files
GlobalHooksRouteHooks plus onRequestsrc/hooks.ts, plugin hooks
PluginA plugin: name plus optional hooksusePlugin()
BurgerServicesThe services on ctx.services, which you extendProviders, ctx.services
WebSocketDataThe data on ws.data, which you extendWebSocket handlers
BurgerWSThe WebSocket object passed to WS handlersWS handlers
openapiOpenAPI metadata, keyed by lowercase methodProgrammatic openapi

Convention files and their types​

Every convention file has a consumer type:

FileTypeExample
burger.build.tsBuildConfigexport default {...} satisfies Partial<BuildConfig>;
src/hooks.tsGlobalHooksexport const beforeRoute: GlobalHooks["beforeRoute"] = [];
src/index.tsServerOptionstyped by new Burger({...})
openapi.config.tsOpenAPIConfigexport default {...} satisfies OpenAPIConfig;
route schema.tsMethodSchemaexport const GET = {...} satisfies MethodSchema;
route openapi.tsOpenAPIMetaexport const GET = {...} satisfies OpenAPIMeta;
route hooks.tsRouteHooksexport const beforeRoute: RouteHooks["beforeRoute"] = [];
route config.tsRouteConfigaugment RouteConfig, then export default {...}
route route.tsdefineRoute(schema, handler)import { GET as GetSchema } from "./schema"; export const GET = defineRoute(GetSchema, (ctx) => ...)
ws ws.tsBurgerWS, WebSocketHandlerstyped handler parameters
src/types.tsapp type extensionsdeclare 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, always Record<string, string> (empty when the route has none). For typed parameters, add a params schema and use ctx.validated.params. See Dynamic Routes.
  • ctx.wildcardParams: always string[] (empty for non-wildcard routes). There is no schema for wildcard segments.
  • ctx.json(): the default is any (the same as the browser Request). Use ctx.json<T>() to give it a type: await ctx.json<{ id: number }>().

Where types live in each feature​