A group mounts controllers under a path prefix and adds hooks to every
route below it. The application has hooks of its own that run for every
request.
group(prefix, { hooks, children }) adds its prefix to every route of its
children. Children are controllers, routes or other groups:
import { createApp, group } from"@tetsujs/core";
constadminOnly= hook.beforeParse((ctx) => {
if (ctx.
req: Request & {
readonly cookies?: Bun.CookieMap;
}
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 routes are GET /api/notes and GET /api/admin/stats, and only the
second runs adminOnly. A route with the path / inside a group answers
at the prefix itself.
A prefix follows the path rules, and also cannot be / alone, contain a
* or declare a :param. A controller is typed where it is written, so
a parameter from the prefix would never appear in ctx.params.
Groups nest only through group(); a controller never contains another
controller. The whole shape of the API is written in one place.
createApp({ hooks }) takes hooks keyed by slot, like a route and a
group. They run for every request the application answers:
import { createApp } from"@tetsujs/core";
import { cors } from"@tetsujs/cors";
import { requestId } from"@tetsujs/request-id";
import { accessLog } from"@tetsujs/request-log";
constid=requestId();
constbrowser=cors({
origin: string | readonly string[]
Which origins are allowed.
A list is matched exactly and echoed back — echoing rather than
returning the list is what the header format requires. "*" allows
every origin, and is refused together with credentials, which the
specification does not permit.
An origin is written as a browser sends it — scheme, host and port,
nothing after: https://app.example.com. Anything else never matches,
so it is refused where it is written, naming the origin it means: a
trailing slash, capitals, a path, a default port. "null" — what a
sandboxed frame or a file: page sends — is taken as it is, and
refused with credentials: any site can send it.
origin: "https://app.example.com" });
constlog=accessLog();
createApp({
hooks: {
beforeParse: [id, browser],
afterResponse: [log],
},
routes: object
The topology: a group, a controller, or an array of either.
routes,
});
For a matched route, each slot runs the application’s hooks first, then
each group’s from the outside in, then the route’s own. onError runs
the other way, route first and application last, so the most specific
hook maps an error first.
A 404, a 405 and an OPTIONS preflight also go through the
lifecycle, but only with the application’s hooks. Group hooks do not run
for them, because no route in the group matched.
This means application-wide logs and rate limits see requests nothing
handled, and a CORS hook on the application can answer a preflight. It
also keeps a group’s guard from refusing a preflight: an authentication
hook on /admin would otherwise turn every preflight into a 401 the
browser cannot read. That is why cors() belongs on the application.
The 404 and 405 also reach the application’s onError hooks as an
HttpError, so one hook sets the error format for everything.
Group and application hooks run for routes they do not know, so they
cannot see a route’s schemas or its hooks’ fields. They can see what
earlier hooks at the same level returned, in the same slot or an
earlier one:
importtype { Requires } from"@tetsujs/core";
import { createApp, hook } from"@tetsujs/core";
import { requestId } from"@tetsujs/request-id";
constid=requestId();
constscope= hook.beforeParse((
ctx: Requires<{
requestId:string;
}>
ctx:Requires<{
requestId: string
requestId:string }>) => ({
tenant: string
tenant:
ctx: Requires<{
requestId:string;
}>
ctx.
req: Request & {
readonly cookies?: Bun.CookieMap;
}
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 topology: a group, a controller, or an array of either.
routes,
});
scope declares what it reads with Requires, and compiles because id
comes first. In the other order it is a compile error that names the
missing field. In beforeResponse, afterResponse and onError such
fields are optional, Requires<{ requestId?: string }>, since the hook
that adds them may not have run.
What a group hook returns does not reach the handlers’ types, for the
reason Context
explains.
A few habits keep it clear what runs for each request.
Make a hook once, in a named constant. Write const log = accessLog(),
then afterResponse: [log]. The slot then reads as a list of names, and
you do not create a new instance each time the surrounding code runs.
Share hooks, not hooks objects. To reuse a set of hooks, declare it
as const and spread each slot where it is mounted:
The topology: a group, a controller, or an array of either.
routes,
});
Without as const, the array loses its element types, and mounting it is
a compile error. Spreading whole hooks objects, or Object.assign,
replaces a slot instead of joining it. For every route under a prefix,
use a group’s hooks.
State lives in the instance. One rateLimit() mounted on two groups
shares one counter. For separate limits, make two instances. To share a
hook’s state across controllers, make it in the composition root and pass
it in; see
Controllers.
An instance runs once per request. The same hook mounted twice in one
route’s chain, for example on a group and on a route under it, is refused
at startup.
You choose the order within a slot. The compiler checks what a hook
needs, not what should come first. Put cors() before every hook that
can refuse a request, so the refusal carries the headers a browser needs
to read it.
A package is one hook. A hook package is a function that takes
options and returns one hook; there is no plugin system. See
Writing a hook package.