inkan

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 plugin

Plugins 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.