Hook System
BurgerAPI uses a hook-based request lifecycle. Six named stages with clear jobs, each returning a defined shape.
Hooks control when code runs on a request.
Plugins extend the application (and may register hooks). Keep them separate.
Six hooks
| Hook | When |
|---|---|
onRequest | Request enters the app |
transform | After routing, before validation (context decoration) |
beforeRoute | After validation, before the handler |
afterRoute | After the handler returns |
mapResponse | Before the response is sent (final headers) |
onError | Any error in the lifecycle |
onRequest → Routing → transform → Validation → beforeRoute
→ Handler → afterRoute → mapResponse
Errors thrown after routing (validation, beforeRoute, handler, afterRoute, mapResponse) jump to onError. Errors thrown in onRequest also run through the global, plugin and framework onError hooks. A request that matches no route is rendered directly (404) without running onError.
Scopes
- Framework
- Plugin
- Global (
src/hooks.ts) - Route (
api/**/hooks.ts)
Request hooks run Framework → Plugin → Global → Route. Response hooks (afterRoute, mapResponse) and error hooks (onError) run nearest-first: Route → Global → Plugin → Framework.
There is no folder or group inheritance of hooks. Each route directory is self-contained.
Route convention files
Alongside route.ts:
hooks.ts: route hooksschema.ts: validationopenapi.ts: OpenAPIconfig.ts: route options (auth, cache, timeout, …)
Types for this feature
Each hook point has its own type. TypeScript checks the return value of your hook against that type.
The types you use (all from burger-api):
ForwardHook: foronRequestandbeforeRoute: returnsResponse(stop),(response) => Response(transform the eventual response), orundefined(continue)ResponseHook: forafterRouteandmapResponse: returnsResponse,(response) => Response, orundefinedErrorHook: foronError: takes(error, ctx), returnsResponseorundefinedRouteHooks: the object with all hook points, for typinghooks.tsfiles
✅ Correct: each hook returns something from its contract:
// hooks.ts
import type { RouteHooks } from "burger-api";
export const beforeRoute: RouteHooks["beforeRoute"] = [
(ctx) => new Response("blocked", { status: 401 }), // stop
() => (res) => res, // continue, but transform the final response
() => undefined, // continue
];
export const afterRoute: RouteHooks["afterRoute"] = [
(ctx) => (res) => res, // change the response
];
❌ Wrong: a hook returns a value that is not in the contract:
export const beforeRoute: RouteHooks["beforeRoute"] = [
() => 42, // ❌ Type 'number' is not assignable
];
The full rules for each return value are on the Hook Return Types page. See the TypeScript overview.
When a route's hooks read ctx.validated, defineHooks(schema, hooks) types every hook's ctx from the route's schema in one step. See Route Hooks.
Check your code: bun run typecheck.