Hooks, plugins, decorators
The lifecycle of a request, and how to extend it.
Hooks run at fixed points of every request, in this order:
.(() => { /* a route was found; the body is not read yet: auth, rate limits */ })
.(() => { /* the input is checked and typed: rules that need it */ })
.((, ) => { .["x-took"] = "…"; }) // every answer, problems too
.((, ) => metrics.observe(., .)) // after it is written
.((, ) => { .. = docsFor(.); });onRequest and preHandler may return a value, and that is the answer, as if
a handler had returned it. A thrown problem() stops the request wherever it is
thrown, and onProblem sees it before it is written.
A plugin is a function that gets a scope of its own. What it adds there (routes,
hooks, decorations) stays there, under its prefix, unless it says shared: true:
import { , , } from "@vxnsin/inkan";
const = (async (, : { : string }) => {
.(({ : 10 })); // only these routes
.(() => { if (..authorization !== .) throw (401, "unauthorized"); });
.("/stats", () => stats());
});
.(, { : ..! }, { : "/admin" });
await .(); // or just listen(): it waits for every pluginPlugins load in the order they are registered, an async one holds back the
ones after it, and one that fails stops listen(). Their routes are routes
like any other: in OpenAPI, on the docs page and in inkan check.
decorate puts a value on every context in its scope, typed:
const = ().("db", pool);
.("/teas/:id", { : .({ : .() }) }, () => .db.find(..));A name the context already has is refused, so a decoration cannot hide params
or another decoration. A route with no hooks anywhere above it takes the same
path it took before there were hooks: they cost nothing until you use them.
For a value made per request (the user of a session, say), decorateRequest makes it the
first time a request reads it. Writing a plugin has that, typed decorations
out of shared plugins, route meta, OpenAPI, the order of hooks ({ order: "last" }),
around() for wrapping every handler of a scope, and stateKey for typed slots in ctx.state.