A hook is a function bound to one slot of the request lifecycle by a
hook.<slot>() factory. This page lists the slots, the factories and the
checks. Lifecycle hooks explains the
model.
req, server, out, route, startedAt, params as strings
an object joins ctx; a Response ends the request; nothing goes on
goes to onError
beforeValidation
after the body is parsed, before any schema runs
the above, body as parsed, rawBody when the route asks
as beforeParse
goes to onError
beforeHandle
after validation, right before the handler
the above, with params, query, headers, cookies and body validated
as beforeParse
goes to onError
beforeResponse
once the response exists: after the handler, a short-circuit or an error
res: Response; validated parts and hook fields optional
a Response replaces ctx.res; anything else is ignored
goes to onError; the remaining hooks run on the error response
afterResponse
as the response goes to Bun, on every outcome
res: SentResponse, without the body; validated parts and hook fields optional
ignored; a promise is not awaited
reported, source: "afterResponse"
onError
when a stage throws
error: unknown; validated parts and hook fields optional
a Response answers; nothing passes the error on
reported, source: "onError"; the next hook tries
parse reads the body when the route declares schema.body, bodyType
or rawBody, and fails with 400 or 413. validate checks every
declared part at once and fails with 422.
Order. Slots run in lifecycle order, whatever order they are written
in. Inside a slot, the array order is the run order. Application hooks
run first, then each group’s from the outermost in, then the route’s.
onError runs the other way, from the route outwards.
Short-circuit. A Response returned before the handler skips the
remaining stages and the handler. It still goes through
beforeResponse, ctx.out.headers and afterResponse. Build a new one
on every call: a body can be read only once.
beforeResponse sees every response, error responses included. Each
hook runs at most once per request: if one throws, the error response
continues from the next hook.
afterResponse hooks start in order without waiting for each other.
Their synchronous part adds to the response’s latency. Read what you
need from ctx.req and ctx.res before the first await.
404, 405 and OPTIONS run the application’s hooks only.
A successful WebSocket handshake runs no response hooks: there is no
response.
SlotBases[slot] is what an unannotated hook’s ctx is typed as. It is
also the most a group or application hook may require, besides what the
hooks before it at the same level contribute.
hook has one factory per slot: hook.beforeParse,
hook.beforeValidation, hook.beforeHandle, hook.beforeResponse,
hook.afterResponse and hook.onError. Each takes a function of ctx and
returns a hook typed with what it requires (its ctx parameter, or the
slot’s base when unannotated) and what it contributes (its return type).
Only a hook made by a factory can be mounted.
The raw incoming request, always available as an escape hatch.
On a request that matched a route, Bun's router delivers its own
request object carrying cookies — a Bun.CookieMap whose mutations
are applied to the response as Set-Cookie automatically, with Bun's
defaults (Path=/; SameSite=Lax). The field is optional because it
does not exist where Bun's router was not involved: the 404 fallback
and unit-tested handlers. ctx.out.headers.append("set-cookie", ...)
is the fallback that works everywhere.
req.
headers: Headers
The headers read-only property of the with the request.
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "GET",
path: "/me"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
path: "/me",
hooks: { beforeParse: [auth] },
handler: (ctx: {
readonlyreq:Request& {
readonlycookies?:Bun.CookieMap;
};
readonlyserver:Bun.Server<unknown>;
readonlyout:Outgoing;
readonlyroute:RouteInfo;
readonlystartedAt:number;
readonlyparams: {};
user:User;
}) => User
The endpoint logic; ctx is fully inferred, never annotate it.
The return type is inferred rather than demanded, and checked twice
over. Against the route's own contract, by the
HandlerResult
bound on R: answering with something response never declared is a
compile error that says so, instead of a structural diff against
Response. And against what the framework can serialize at all, by
the intersected
ValidateResult
: a stream handed over bare is
refused whether or not the route declared anything, because that is
the case no contract covers — without a response schema
HandlerResult is unknown and accepts every value there is.
handler: (
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
user: User;
}
ctx) =>
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
user: User;
}
ctx.user,
user: User
});
What a returned object contributes:
its own enumerable fields, typed in every later hook and in the handler.
Return a plain object literal: a class instance’s methods and getters
live on its prototype and are not copied;
optional fields when the hook may also return nothing
(return user ? { user } : undefined);
never req, server, out, route, res, error, startedAt or
rawBody: the pipeline owns them and drops them from a returned object.
A Response contributes nothing; it short-circuits instead. An onError
hook returns a Response or nothing; returning an object is a compile
error.
A returned cookies object is checked like the request’s cookies: under
a signed name, a value whose signature does not hold is dropped.
A reusable hook declares what it needs instead of where it sits. Mounting
it where nothing provides that is a compile error naming the missing
field:
constwithOrder= hook.beforeHandle(
async (
ctx: Requires<{
params: {
id:string;
};
user:User;
}>
ctx:Requires<{
params: {
id: string;
}
params: {
id: string
id:string };
user: User
user:User }>) => {
const
constorder:Order|undefined
order=await
constorders: {
find(id:string):Promise<Order|undefined>;
}
orders.find(
ctx: Requires<{
params: {
id:string;
};
user:User;
}>
ctx.
params: {
id: string;
}
params.
id: string
id);
if (!
constorder:Order|undefined
order||
constorder:Order
order.
ownerId: number
ownerId!==
ctx: Requires<{
params: {
id:string;
};
user:User;
}>
ctx.
user: User
user.
id: number
id) thrownewHttpError(404);
return {
order: Order
order };
},
);
route({
method: "POST"
The method this route answers.
Kept as a literal rather than widened to
Method
: the method is
half of a route's identity, and an application that remembers its
routes — for a generated client, for tooling — needs to know which one
this is.
method: "POST",
path: "/orders/:id/cancel"
Route path with :param segments, e.g. "/orders/:id/cancel".
Must start with /, contain no empty segments and no trailing slash;
a malformed literal is a compile error.
The endpoint logic; ctx is fully inferred, never annotate it.
The return type is inferred rather than demanded, and checked twice
over. Against the route's own contract, by the
HandlerResult
bound on R: answering with something response never declared is a
compile error that says so, instead of a structural diff against
Response. And against what the framework can serialize at all, by
the intersected
ValidateResult
: a stream handed over bare is
refused whether or not the route declared anything, because that is
the case no contract covers — without a response schema
HandlerResult is unknown and accepts every value there is.