<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Orinda Felix Ochieng</title>
    <description>The latest articles on DEV Community by Orinda Felix Ochieng (@forinda).</description>
    <link>https://dev.to/forinda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F699687%2Fefcf47e2-7a04-4b9e-a65c-846c774a23ba.jpeg</url>
      <title>DEV Community: Orinda Felix Ochieng</title>
      <link>https://dev.to/forinda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/forinda"/>
    <language>en</language>
    <item>
      <title>Building Custom Context Decorators in KickJS</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:50:09 +0000</pubDate>
      <link>https://dev.to/forinda/building-custom-context-decorators-in-kickjs-1n9h</link>
      <guid>https://dev.to/forinda/building-custom-context-decorators-in-kickjs-1n9h</guid>
      <description>&lt;p&gt;In Express you reach for &lt;code&gt;req.locale = ...&lt;/code&gt; inside a middleware and hope the&lt;br&gt;
downstream handler remembers what type it is. KickJS gives you a better home&lt;br&gt;
for that data: a &lt;strong&gt;context decorator&lt;/strong&gt;. You declare the key once, write a&lt;br&gt;
typed &lt;code&gt;resolve(ctx)&lt;/code&gt; function, register it at the scope it belongs to, and&lt;br&gt;
every handler in that scope can call &lt;code&gt;ctx.get('locale')&lt;/code&gt; with full type&lt;br&gt;
safety. Anything you'd hang off &lt;code&gt;req&lt;/code&gt; — request id, locale, the audit actor,&lt;br&gt;
feature flags, an idempotency key — should live here instead. This article&lt;br&gt;
is the field guide: the two factories, the full options surface, the five&lt;br&gt;
registration scopes and how they fight, how to read values back from&lt;br&gt;
handlers and services, the boot-time errors you should know on sight, and&lt;br&gt;
three patterns worth stealing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two factories
&lt;/h2&gt;

&lt;p&gt;KickJS ships two factories, and the difference is whether your resolver&lt;br&gt;
needs to see the HTTP request.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defineHttpContextDecorator&lt;/code&gt; is for HTTP routes. The &lt;code&gt;ctx&lt;/code&gt; argument it hands&lt;br&gt;
your resolver is a &lt;code&gt;RequestContext&lt;/code&gt; — &lt;code&gt;ctx.req&lt;/code&gt;, &lt;code&gt;ctx.headers&lt;/code&gt;, &lt;code&gt;ctx.params&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;ctx.body&lt;/code&gt;, plus the shared &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;requestId&lt;/code&gt; surface. Reach for it&lt;br&gt;
when the value is derived from the wire: parsing an &lt;code&gt;Accept-Language&lt;/code&gt;&lt;br&gt;
header, lifting an &lt;code&gt;X-Idempotency-Key&lt;/code&gt;, decoding a cookie, anything that&lt;br&gt;
only makes sense in the HTTP transport.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defineContextDecorator&lt;/code&gt; is transport-agnostic. The &lt;code&gt;ctx&lt;/code&gt; it gives you is&lt;br&gt;
an &lt;code&gt;ExecutionContext&lt;/code&gt; with only &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;, and &lt;code&gt;requestId&lt;/code&gt; — no &lt;code&gt;req&lt;/code&gt;,&lt;br&gt;
no headers. That's deliberate: a contributor built on this factory will&lt;br&gt;
also work the same way inside a CLI command, a queue worker, a cron job,&lt;br&gt;
or whatever non-HTTP transport adapter you bolt on next. Reach for it when&lt;br&gt;
the value is computed from other context entries (&lt;code&gt;dependsOn&lt;/code&gt;) or from DI&lt;br&gt;
services (&lt;code&gt;deps&lt;/code&gt;), not from the request itself. A &lt;code&gt;LoadFeatureFlags&lt;/code&gt;&lt;br&gt;
contributor that takes the resolved actor id from &lt;code&gt;dependsOn&lt;/code&gt; and asks a&lt;br&gt;
flag service is a perfect fit — it doesn't care whether the call came in&lt;br&gt;
over HTTP.&lt;/p&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;start with &lt;code&gt;defineContextDecorator&lt;/code&gt; and only escalate&lt;br&gt;
to the HTTP variant if your resolver actually touches &lt;code&gt;ctx.req&lt;/code&gt;.&lt;/strong&gt; Keeping&lt;br&gt;
contributors transport-agnostic where you can means the day you mount the&lt;br&gt;
same module under a queue worker, every contributor still works.&lt;/p&gt;
&lt;h2&gt;
  
  
  The options surface
&lt;/h2&gt;

&lt;p&gt;Both factories take the same options object. Six fields, each pulling its&lt;br&gt;
weight.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineHttpContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;LocaleService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;accept-language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt; is the string handle the rest of the system uses to find this
value. It's also the field you'll augment in &lt;code&gt;ContextMeta&lt;/code&gt; for type
safety. Pick something stable — renaming a key is a breaking change for
every handler that reads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resolve(ctx, deps?)&lt;/code&gt;&lt;/strong&gt; is the work itself. Sync or async, returns the
value to be stored under &lt;code&gt;key&lt;/code&gt;. Throwing here is a real error unless
&lt;code&gt;optional&lt;/code&gt; or &lt;code&gt;onError&lt;/code&gt; says otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deps&lt;/code&gt;&lt;/strong&gt; is a tuple of DI tokens. Whatever you list shows up as the
second argument to &lt;code&gt;resolve&lt;/code&gt;, already constructed and scoped to the
current request. Mark it &lt;code&gt;as const&lt;/code&gt; so the tuple types survive into the
argument signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dependsOn&lt;/code&gt;&lt;/strong&gt; declares which other context keys must be resolved before
this one runs. KickJS topologically sorts contributors at boot and
guarantees the order. If you read &lt;code&gt;ctx.get('actor')&lt;/code&gt; inside &lt;code&gt;resolve&lt;/code&gt;,
put &lt;code&gt;'actor'&lt;/code&gt; in &lt;code&gt;dependsOn&lt;/code&gt; — otherwise you're racing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;optional: true&lt;/code&gt;&lt;/strong&gt; swallows resolver errors silently. The key simply
ends up &lt;code&gt;undefined&lt;/code&gt;. Use it for genuinely optional signals (a missing
idempotency header is fine; a missing tenant probably isn't).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onError(err, ctx)&lt;/code&gt;&lt;/strong&gt; is the recovery hatch. If &lt;code&gt;resolve&lt;/code&gt; throws and
&lt;code&gt;onError&lt;/code&gt; is defined, its return value gets stored under &lt;code&gt;key&lt;/code&gt;. Use it
for sane defaults — a failed locale lookup falling back to &lt;code&gt;'en'&lt;/code&gt;, a
missing feature-flag service falling back to an empty set.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five places to register a contributor
&lt;/h2&gt;

&lt;p&gt;A decorator definition is inert until you register it somewhere. KickJS&lt;br&gt;
offers five scopes, and they form a strict precedence chain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method &amp;gt; Class &amp;gt; Module &amp;gt; Adapter &amp;gt; Global.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Method&lt;/strong&gt; — &lt;code&gt;@LoadLocale()&lt;/code&gt; placed on a single handler. Highest
precedence; a route-specific override that beats every wider scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class&lt;/strong&gt; — &lt;code&gt;@LoadLocale()&lt;/code&gt; on a controller class. Applies to every
handler on that controller. The right scope when a single resource is
the only thing that needs the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module&lt;/strong&gt; — return it from &lt;code&gt;AppModule.contributors?()&lt;/code&gt;. Every route
the module mounts gets it. Best for module-domain values that don't
belong to the whole app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter&lt;/strong&gt; — return it from &lt;code&gt;AppAdapter.contributors?()&lt;/code&gt;. Every route
in the app gets it. This is where cross-cutting concerns (request id,
trace context, the actor lookup) belong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global&lt;/strong&gt; — pass it via &lt;code&gt;bootstrap({ contributors: [...] })&lt;/code&gt;. App-wide
defaults, lowest precedence — anything below can override.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The precedence rule does two different things depending on whether the&lt;br&gt;
collision is at the same level or across levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same level, same key&lt;/strong&gt;: KickJS throws &lt;code&gt;DuplicateContributorError&lt;/code&gt; at&lt;br&gt;
boot. Two adapter-level contributors both producing &lt;code&gt;'locale'&lt;/code&gt; is a bug,&lt;br&gt;
not a configuration question, and the framework refuses to guess. Pick&lt;br&gt;
one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross level, same key&lt;/strong&gt;: the higher-precedence one silently wins. A&lt;br&gt;
method decorator beats a class decorator beats a module contributor beats&lt;br&gt;
an adapter contributor beats a global one. No warning, no error — that's&lt;br&gt;
the point of overrides. Define &lt;code&gt;LoadLocale&lt;/code&gt; adapter-wide as the default,&lt;br&gt;
then attach a &lt;code&gt;LoadLocale({ source: 'query' })&lt;/code&gt; method-level decorator on&lt;br&gt;
the one admin route that wants the locale taken from &lt;code&gt;?locale=&lt;/code&gt; instead&lt;br&gt;
of headers, and the override happens cleanly.&lt;/p&gt;

&lt;p&gt;A useful mental model: &lt;strong&gt;register where the responsibility actually&lt;br&gt;
lives.&lt;/strong&gt; Cross-cutting concerns belong on the adapter. Module-domain&lt;br&gt;
values belong on the module. One-off overrides belong on the method.&lt;br&gt;
Global is for defaults you'll let any of the above replace.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the value back
&lt;/h2&gt;

&lt;p&gt;Inside a handler you have &lt;code&gt;ctx&lt;/code&gt;, so it's just &lt;code&gt;ctx.get(key)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;RequestCtx&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// typed via ContextMeta&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a service — somewhere with no &lt;code&gt;ctx&lt;/code&gt; in scope — you reach for the&lt;br&gt;
async-local-storage helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getRequestValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getRequestStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getRequestValue&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// T | undefined&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// throws outside a request&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getRequestValue&lt;/code&gt; is the safe one — returns &lt;code&gt;T | undefined&lt;/code&gt;, fine to call&lt;br&gt;
anywhere. &lt;code&gt;getRequestStore&lt;/code&gt; throws if there's no active request frame, so&lt;br&gt;
use it only when you're certain you're inside one (a request handler, a&lt;br&gt;
service called from one). The async-local-storage backing means it works&lt;br&gt;
through &lt;code&gt;await&lt;/code&gt; boundaries without you threading &lt;code&gt;ctx&lt;/code&gt; everywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  Errors and recovery
&lt;/h2&gt;

&lt;p&gt;Two boot-time errors are worth recognizing on sight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MissingContributorError&lt;/code&gt;&lt;/strong&gt; — a contributor declares &lt;code&gt;dependsOn:
['actor']&lt;/code&gt; but no contributor in the resolved chain produces &lt;code&gt;'actor'&lt;/code&gt;.
Thrown at boot, before any request is served. Fix: register the missing
contributor or remove the dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ContributorCycleError&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;A&lt;/code&gt; depends on &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt; depends on &lt;code&gt;A&lt;/code&gt;.
Also thrown at boot. Fix: break the cycle, usually by extracting the
shared piece into a third contributor both depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At request time, &lt;code&gt;optional&lt;/code&gt; and &lt;code&gt;onError&lt;/code&gt; are your two recovery levers.&lt;br&gt;
&lt;code&gt;optional: true&lt;/code&gt; says "if this fails, just leave the key undefined and&lt;br&gt;
keep going." &lt;code&gt;onError&lt;/code&gt; says "if this fails, here's the value to use&lt;br&gt;
instead." Reach for &lt;code&gt;optional&lt;/code&gt; when downstream code already handles&lt;br&gt;
&lt;code&gt;undefined&lt;/code&gt;; reach for &lt;code&gt;onError&lt;/code&gt; when there's a meaningful default.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three patterns worth stealing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Per-route override.&lt;/strong&gt; Define an adapter-level &lt;code&gt;LoadLocale&lt;/code&gt; that reads&lt;br&gt;
&lt;code&gt;Accept-Language&lt;/code&gt;. On the one admin route that takes &lt;code&gt;?locale=&lt;/code&gt; from the&lt;br&gt;
query string, attach a method-level &lt;code&gt;@LoadLocale({ source: 'query' })&lt;/code&gt;.&lt;br&gt;
The method scope beats the adapter scope silently — no &lt;code&gt;if&lt;/code&gt; branches in&lt;br&gt;
the resolver, no flags, just precedence doing the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin-shipping.&lt;/strong&gt; When you publish a contributor as part of a library,&lt;br&gt;
export both the &lt;code&gt;@DecoratorName&lt;/code&gt; form and the underlying &lt;code&gt;.registration&lt;/code&gt;&lt;br&gt;
object. Consumers who want it on every route call &lt;code&gt;bootstrap({&lt;br&gt;
contributors: [LoadFeatureFlags.registration] })&lt;/code&gt;. Consumers who want it&lt;br&gt;
on a single controller use &lt;code&gt;@LoadFeatureFlags()&lt;/code&gt;. Same definition, both&lt;br&gt;
ergonomics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadFeatureFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;FlagsService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// Consumers reach for either LoadFeatureFlags() or LoadFeatureFlags.registration.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Factory wrapper for parameterized decorators.&lt;/strong&gt; When the same&lt;br&gt;
contributor needs different config per call site, wrap the factory in a&lt;br&gt;
function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadRequestStartedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;defineContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requestStartedAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;LoadRequestStartedAt()&lt;/code&gt; uses the real clock, &lt;code&gt;LoadRequestStartedAt(()&lt;br&gt;
=&amp;gt; 0)&lt;/code&gt; is trivially mockable in tests, and the type signature stays clean.&lt;/p&gt;
&lt;h2&gt;
  
  
  A note on type safety
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ctx.get('locale')&lt;/code&gt; returns &lt;code&gt;unknown&lt;/code&gt; until you augment the type. Drop&lt;br&gt;
this once per key, in a &lt;code&gt;.d.ts&lt;/code&gt; file or near the contributor definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;requestStartedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlySet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that every &lt;code&gt;ctx.get&lt;/code&gt; is fully typed. There's a separate article on&lt;br&gt;
the augmentation mechanics — the short version is: yes, do it, every key,&lt;br&gt;
every time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Unit-test the resolver in isolation. Mock its &lt;code&gt;deps&lt;/code&gt;, hand it a stub &lt;code&gt;ctx&lt;/code&gt;&lt;br&gt;
with the &lt;code&gt;dependsOn&lt;/code&gt; keys pre-seeded via &lt;code&gt;set&lt;/code&gt;, assert the return value.&lt;br&gt;
That's it — no app boot, no HTTP, no DI container.&lt;/p&gt;

&lt;p&gt;Integration-test by booting the real adapter through &lt;code&gt;createTestApp&lt;/code&gt; and&lt;br&gt;
hitting it with supertest. The contributor chain runs end-to-end and you&lt;br&gt;
verify what handlers actually see.&lt;/p&gt;

&lt;p&gt;For service-layer reads — code that calls &lt;code&gt;getRequestValue&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;getRequestStore&lt;/code&gt; — wrap the assertion in a &lt;code&gt;requestStore.run&lt;/code&gt; frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;requestStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preferredLocale&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the same async-local-storage backing the framework uses at&lt;br&gt;
runtime, so the production code path runs unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/context-decorators.html" rel="noopener noreferrer"&gt;KickJS context-decorators guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/api/core.html" rel="noopener noreferrer"&gt;KickJS context-meta augmentation note&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>decorators</category>
    </item>
    <item>
      <title>The KickJS Lifecycle — Bootstrap, Runtime, Shutdown</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:47:24 +0000</pubDate>
      <link>https://dev.to/forinda/the-kickjs-lifecycle-bootstrap-runtime-shutdown-cpl</link>
      <guid>https://dev.to/forinda/the-kickjs-lifecycle-bootstrap-runtime-shutdown-cpl</guid>
      <description>&lt;p&gt;Every framework has a lifecycle, but most leave you to reverse-engineer it from console logs and stack traces. KickJS is unusually explicit about its phases — there are three of them, the firing order inside each is documented, and the framework will throw at boot if you wire up something that violates the contract. That clarity is a feature: it means you can pick the right hook the first time instead of discovering at 2am that your DB pool was constructed before the config adapter had loaded its env vars. This article walks the three phases, the ordering rules inside each, and the contracts you can rely on at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three phases
&lt;/h2&gt;

&lt;p&gt;A KickJS process has exactly three phases, and they happen in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup&lt;/strong&gt; — everything that runs once during &lt;code&gt;bootstrap()&lt;/code&gt;. Config loads, adapters mount, modules register, routes are wired, &lt;code&gt;listen()&lt;/code&gt; fires. By the end of this phase, the app is accepting connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt; — the per-request phase. For every inbound HTTP request, a fresh AsyncLocalStorage frame is opened, middleware and contributors run, the handler resolves, and the frame closes. This phase repeats indefinitely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shutdown&lt;/strong&gt; — triggered by &lt;code&gt;SIGTERM&lt;/code&gt;, &lt;code&gt;SIGINT&lt;/code&gt;, or an explicit &lt;code&gt;app.shutdown()&lt;/code&gt; call. Every adapter's &lt;code&gt;shutdown()&lt;/code&gt; runs concurrently, the HTTP server closes, and the process exits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setup runs once. Runtime runs N times. Shutdown runs once. Knowing which phase you're in tells you which APIs are safe to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup phase, step by step
&lt;/h2&gt;

&lt;p&gt;The setup phase has a deterministic firing order. Here it is end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bootstrap()
  │
  ├─ 1. Adapter beforeMount hooks      (config / env adapters resolve here)
  ├─ 2. Hardened defaults + ALS frame  (request-scope is now installable)
  ├─ 3. Adapter middleware mount       (beforeGlobal → afterGlobal
  │                                     → beforeRoutes → afterRoutes)
  ├─ 4. Plugin + user middleware       (in registration order)
  ├─ 5. Security defaults              (helmet, CORS, body limits)
  ├─ 6. Module registration            (DI registry populated)
  ├─ 7. Route mounting                 (per-controller onRouteMount fires)
  ├─ 8. Adapter beforeStart hooks      (every route is known, listen() is not)
  ├─ 9. server.listen()                (socket bound, accepting connections)
  └─ 10. Adapter afterStart hooks      (announce, register with discovery, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things are worth pinning down at each step.&lt;/p&gt;

&lt;p&gt;By the time &lt;strong&gt;step 1&lt;/strong&gt; finishes, anything an adapter wanted to read from the environment or from a config file is loaded. This is where a config adapter's &lt;code&gt;beforeMount&lt;/code&gt; parses &lt;code&gt;.env&lt;/code&gt; and exposes typed values. Until this step completes, do not assume any config is present.&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;step 2&lt;/strong&gt;, the AsyncLocalStorage substrate is installed but no request frame exists yet — there's nothing to scope to. Code that calls &lt;code&gt;getRequestContext()&lt;/code&gt; here will (correctly) get &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps 3–5&lt;/strong&gt; layer middleware. The four-phase model — &lt;code&gt;beforeGlobal&lt;/code&gt;, &lt;code&gt;afterGlobal&lt;/code&gt;, &lt;code&gt;beforeRoutes&lt;/code&gt;, &lt;code&gt;afterRoutes&lt;/code&gt; — gives adapters a predictable place to inject cross-cutting concerns: a tracing adapter wraps everything in &lt;code&gt;beforeGlobal&lt;/code&gt;; an auth adapter slots in at &lt;code&gt;beforeRoutes&lt;/code&gt;; a response-shaping adapter cleans up at &lt;code&gt;afterRoutes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;step 6&lt;/strong&gt;, the DI registry is fully populated. Every &lt;code&gt;@Service&lt;/code&gt; is constructible. Every &lt;code&gt;registerInstance(...)&lt;/code&gt; call has run. This is the earliest point where you can reliably resolve dependencies by token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt; is where each controller's &lt;code&gt;onRouteMount&lt;/code&gt; notification fires — useful for adapters that want to introspect or decorate the route table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8 — &lt;code&gt;beforeStart&lt;/code&gt; — is the most useful and most often misused hook.&lt;/strong&gt; Every module has registered, every route is mounted, but &lt;code&gt;listen()&lt;/code&gt; has not yet fired. This is the right slot for "construct + register in DI" code that needs the route table populated: schema generators, OpenAPI emitters, route-conditional health checks. It is the &lt;em&gt;wrong&lt;/em&gt; slot to start accepting external traffic — the socket isn't open yet.&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;step 10 — &lt;code&gt;afterStart&lt;/code&gt;&lt;/strong&gt; — the server is live. This is where you announce yourself to a service registry, log the bound port, or warm a cache that depends on the app being reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Within-phase ordering
&lt;/h2&gt;

&lt;p&gt;Two ordering rules apply &lt;em&gt;inside&lt;/em&gt; setup, and they answer most "why did X run before Y?" questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adapters run in the order they appear in the &lt;code&gt;adapters&lt;/code&gt; array.&lt;/strong&gt; If you put &lt;code&gt;loggingAdapter&lt;/code&gt; before &lt;code&gt;authAdapter&lt;/code&gt; in your &lt;code&gt;bootstrap&lt;/code&gt; config, logging's &lt;code&gt;beforeMount&lt;/code&gt; runs first, its &lt;code&gt;beforeGlobal&lt;/code&gt; middleware runs first, its &lt;code&gt;beforeStart&lt;/code&gt; runs first, and so on. The array is the source of truth — there's no hidden priority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contributors are topologically sorted.&lt;/strong&gt; A contributor is anything that adds behavior to the request pipeline — a middleware, an interceptor, a guard. KickJS sorts them at boot using a fixed precedence — &lt;code&gt;method &amp;gt; class &amp;gt; module &amp;gt; adapter &amp;gt; global&lt;/code&gt; — so a method-scoped guard always runs inside a class-scoped one, and both run inside any global middleware. On top of that, contributors can declare an explicit &lt;code&gt;dependsOn&lt;/code&gt; list. The framework validates the resulting graph at boot: cycles throw, missing dependencies throw, ambiguous orderings throw. You find out before &lt;code&gt;listen()&lt;/code&gt; fires, not under load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Synthetic — declares "rate-limit must run after auth"&lt;/span&gt;
&lt;span class="nf"&gt;defineContributor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rate-limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination — array order for adapters, topo-sort for contributors — gives you predictable composition without surprise re-ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime: the per-request frame
&lt;/h2&gt;

&lt;p&gt;Once the server is listening, every request goes through the same shape.&lt;/p&gt;

&lt;p&gt;A request lands. KickJS opens a new AsyncLocalStorage frame for it — a per-request "bag" that follows every &lt;code&gt;await&lt;/code&gt; boundary inside the handler. The middleware chain runs. The contributor pipeline runs in topo-sorted order. The matched route handler runs. The response is written. The frame closes.&lt;/p&gt;

&lt;p&gt;There are three layers that share this bag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Middleware&lt;/code&gt; decorators&lt;/strong&gt; — your own middleware, mounted in step 4 of setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The contributor wrapper&lt;/strong&gt; — adapter and module contributors, sorted by precedence and &lt;code&gt;dependsOn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The main handler&lt;/strong&gt; — the controller method itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three see the same &lt;code&gt;ctx&lt;/code&gt;. Anything one layer puts there with &lt;code&gt;ctx.set('userId', id)&lt;/code&gt; is visible to the next layer with &lt;code&gt;ctx.get('userId')&lt;/code&gt;. The bag is &lt;em&gt;strictly&lt;/em&gt; per-request: no other concurrent request can read it, even on the same Node thread, because AsyncLocalStorage isolates it for you. That is what makes "current user", "current tenant", "current trace span" safe to store without threading them through every function signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Synthetic — three layers, one bag&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;resolveTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// middleware&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;span&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;             &lt;span class="c1"&gt;// contributor reads it&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// handler reads it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Shutdown: parallel and best-effort
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;SIGTERM&lt;/code&gt; or &lt;code&gt;SIGINT&lt;/code&gt; arrives — or your code calls &lt;code&gt;app.shutdown()&lt;/code&gt; — KickJS runs every adapter's &lt;code&gt;shutdown()&lt;/code&gt; method &lt;strong&gt;concurrently&lt;/strong&gt;, wrapped in &lt;code&gt;Promise.allSettled&lt;/code&gt;. Two properties fall out of that choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent.&lt;/strong&gt; Adapters do not block each other. A slow database adapter draining its connection pool does not delay a fast logging adapter from flushing its buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure-isolated.&lt;/strong&gt; If one adapter's &lt;code&gt;shutdown()&lt;/code&gt; rejects, the others still complete. &lt;code&gt;allSettled&lt;/code&gt; never short-circuits. You will see the rejection in the framework's shutdown report, but it cannot starve siblings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters for resource ownership. Each adapter is responsible for the resources &lt;em&gt;it&lt;/em&gt; opened: the DB adapter closes its pool, the queue adapter drains its consumers, the cache adapter flushes pending writes. Nobody else cleans up for you, and nobody else's failure prevents you from cleaning up. The contract is "every adapter gets a chance."&lt;/p&gt;

&lt;p&gt;After all &lt;code&gt;shutdown()&lt;/code&gt; promises settle, the HTTP server closes its listener and the process exits. If you need a hard deadline, wrap your individual &lt;code&gt;shutdown()&lt;/code&gt; body in its own timeout — the framework intentionally does not impose one.&lt;/p&gt;

&lt;h2&gt;
  
  
  HMR: the dev-mode caveat
&lt;/h2&gt;

&lt;p&gt;In development, &lt;code&gt;kick dev&lt;/code&gt; runs the app under Hot Module Replacement. When you save a file, the running app is &lt;strong&gt;torn down and re-bootstrapped&lt;/strong&gt; — the full shutdown phase fires, then the full setup phase runs again, in the same Node process.&lt;/p&gt;

&lt;p&gt;This is where adapters that own connections, sockets, file watchers, or background timers earn their keep. If your adapter's &lt;code&gt;shutdown()&lt;/code&gt; doesn't actually drain — if it leaves a Postgres pool open, a Redis subscription connected, an interval ticking — you will leak those handles every time you save a file. Ten saves later your dev box has fifty zombie Postgres connections and you're staring at "too many clients" errors that don't exist in production.&lt;/p&gt;

&lt;p&gt;The rule: &lt;em&gt;write &lt;code&gt;shutdown()&lt;/code&gt; as if it runs every five seconds&lt;/em&gt;, because in dev mode it effectively does. See the framework's separate HMR guide for the full reload protocol and the patterns for hot-swappable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  App-scoped vs request-scoped state
&lt;/h2&gt;

&lt;p&gt;The mental model is simple, and it's the single thing most worth internalizing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App-scoped state&lt;/strong&gt; lives in the DI container. It's registered once, during setup. A &lt;code&gt;registerInstance(DbPoolToken, pool)&lt;/code&gt; call survives every request the process ever serves. Singletons, config objects, connection pools, cache clients, metric emitters — all app-scoped. They outlive any one request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request-scoped state&lt;/strong&gt; lives in the AsyncLocalStorage bag. Anything you put there with &lt;code&gt;ctx.set('key', value)&lt;/code&gt; exists only for that request and is invisible to any other concurrent request. Current user, current tenant, current trace span, current locale — all request-scoped. They die when the response is written.&lt;/p&gt;

&lt;p&gt;If you find yourself reaching for a global mutable variable to share state between layers, you almost certainly want the bag. If you find yourself reconstructing the same object for every request, you almost certainly want DI. Get this distinction right and most of your "where does this go?" questions answer themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/lifecycle.html" rel="noopener noreferrer"&gt;KickJS lifecycle guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>lifecycle</category>
    </item>
    <item>
      <title>KickJS Asset Manager — Type-Safe File Resolution from Dev to Dist</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:45:29 +0000</pubDate>
      <link>https://dev.to/forinda/kickjs-asset-manager-type-safe-file-resolution-from-dev-to-dist-3ih9</link>
      <guid>https://dev.to/forinda/kickjs-asset-manager-type-safe-file-resolution-from-dev-to-dist-3ih9</guid>
      <description>&lt;p&gt;There is a familiar little discomfort that shows up the first time a Node.js service needs to read something off disk that is not source code. An email template. A JSON schema. A seed fixture. A vendored PDF. The first version is always a one-liner: &lt;code&gt;fs.readFile(path.join(__dirname, '../templates/welcome.ejs'))&lt;/code&gt;. That works on your laptop. It also works in CI. And then you ship to production, your bundler flattens the output, &lt;code&gt;__dirname&lt;/code&gt; points at &lt;code&gt;dist/handlers/&lt;/code&gt; instead of &lt;code&gt;src/handlers/&lt;/code&gt;, the relative climb misses the templates directory, and the first user who triggers a password reset gets a 500.&lt;/p&gt;

&lt;p&gt;The pragmatic fix is to scatter &lt;code&gt;process.env.NODE_ENV === 'production' ? '../../dist/templates' : '../templates'&lt;/code&gt; across the codebase. The principled fix is to admit that on-disk assets are a build-time concern that deserves its own little type system. That is what the KickJS Asset Manager is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Server-side asset resolution has three properties that conspire against you.&lt;/p&gt;

&lt;p&gt;First, the &lt;strong&gt;path differs between dev and dist&lt;/strong&gt;. Dev runs against &lt;code&gt;src/&lt;/code&gt;, dist runs against a built output that may have a different layout, possibly minified, possibly bundled, possibly emitted by a different tool than the one that compiled your handlers.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;the keys are stringly-typed&lt;/strong&gt;. &lt;code&gt;readFile('templates/welcome.ejs')&lt;/code&gt; is just a string. Rename the file, forget to update the call site, and TypeScript gives you nothing — you find out at runtime, usually from a Sentry alert, usually from an end user.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;the cleanup story is bad&lt;/strong&gt;. &lt;code&gt;NODE_ENV&lt;/code&gt; checks, &lt;code&gt;try/catch&lt;/code&gt; fallbacks, conditional &lt;code&gt;__dirname&lt;/code&gt; math, helper functions that paper over the asymmetry — every codebase with on-disk assets accretes a small graveyard of these. They are correct individually and incoherent collectively.&lt;/p&gt;

&lt;p&gt;The Asset Manager replaces all three problems with a single mental model: at build time, every asset you care about is copied into a known location and recorded in a manifest. At runtime, you address those assets through a typed proxy whose shape is generated from the manifest. There is no &lt;code&gt;NODE_ENV&lt;/code&gt;, no &lt;code&gt;__dirname&lt;/code&gt; arithmetic, and no string keys that the compiler cannot check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: a manifest plus a typed proxy
&lt;/h2&gt;

&lt;p&gt;Two artifacts do the heavy lifting.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;.kickjs-assets.json&lt;/code&gt;, a manifest emitted into &lt;code&gt;dist/&lt;/code&gt; during build. It is the canonical answer to "where, on this filesystem, is the asset named &lt;code&gt;mails.welcome&lt;/code&gt;?". It is a flat JSON object — categories at the top level, slugs underneath, absolute or &lt;code&gt;dist&lt;/code&gt;-relative paths as values.&lt;/p&gt;

&lt;p&gt;The second is &lt;code&gt;.kickjs/types/assets.d.ts&lt;/code&gt;, a generated declaration file that augments the framework-supplied &lt;code&gt;KickAssets&lt;/code&gt; interface. The interface mirrors the manifest's shape exactly: &lt;code&gt;KickAssets['mails']['welcome']&lt;/code&gt; exists if and only if the manifest has an entry for &lt;code&gt;mails.welcome&lt;/code&gt;. That augmentation is what gives the proxy its types.&lt;/p&gt;

&lt;p&gt;The runtime side is a &lt;code&gt;Proxy&lt;/code&gt; (or several flavours of one — see below). When you write &lt;code&gt;assets.mails.welcome()&lt;/code&gt;, the proxy walks the path &lt;code&gt;['mails', 'welcome']&lt;/code&gt;, looks the slug up in the manifest, and returns the resolved file path. No filesystem reads, no environment checks, no &lt;code&gt;path.join&lt;/code&gt; math in your handlers.&lt;/p&gt;

&lt;p&gt;The result is that asset resolution becomes a pure function from a typed key to a string path, and every typo is a compile error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways to consume an asset
&lt;/h2&gt;

&lt;p&gt;KickJS gives you four consumption patterns. They all resolve through the same manifest — the difference is purely ergonomic.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The ambient nested proxy
&lt;/h3&gt;

&lt;p&gt;The default and most common pattern. Import once, address anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;assets&lt;/code&gt; is a deeply-nested proxy whose shape comes from the augmented &lt;code&gt;KickAssets&lt;/code&gt; interface. IDE autocomplete works at every level: &lt;code&gt;assets.&lt;/code&gt; shows categories, &lt;code&gt;assets.mails.&lt;/code&gt; shows slugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;useAssets()&lt;/code&gt; — the hook factory
&lt;/h3&gt;

&lt;p&gt;Sometimes you want the resolver as a value, not a singleton. &lt;code&gt;useAssets()&lt;/code&gt; returns the same proxy but as a factory call, which is convenient when you want to swap implementations in tests or pass the resolver through a DI container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAssets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAssets&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nf"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the form to reach for when you would otherwise be tempted to mock the import.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;@Asset()&lt;/code&gt; — the class field decorator
&lt;/h3&gt;

&lt;p&gt;For class-based services where you want the resolution to be lazy and declarative.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Asset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mails/welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;welcomeTpl&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderEjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;welcomeTpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decorator installs a getter that resolves on first access. The string argument is type-checked against the manifest, so &lt;code&gt;@Asset('mails/does-not-exist')&lt;/code&gt; is a compile error.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;resolveAsset()&lt;/code&gt; — the dynamic escape hatch
&lt;/h3&gt;

&lt;p&gt;When the key really is dynamic — an admin choosing a template at runtime, a feature-flagged variant — fall back to the function form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveAsset&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// slug: string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You lose the per-key compile-time check, but the category is still typed and a missing slug throws a meaningful runtime error rather than returning a bogus path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build → manifest → runtime
&lt;/h2&gt;

&lt;p&gt;The split between build and runtime is what makes the whole thing tractable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kick build&lt;/code&gt; (or the focused &lt;code&gt;kick build:assets&lt;/code&gt;) reads &lt;code&gt;assetMap&lt;/code&gt; from &lt;code&gt;kick.config.ts&lt;/code&gt;. Each entry has a &lt;code&gt;src&lt;/code&gt; path (required), an optional &lt;code&gt;dest&lt;/code&gt; (defaults to the entry's name), and an optional &lt;code&gt;glob&lt;/code&gt; (defaults to &lt;code&gt;**/*&lt;/code&gt;). For every entry, the builder walks the glob, copies matching files to &lt;code&gt;dist/&amp;lt;dest&amp;gt;/&lt;/code&gt;, and records each one in &lt;code&gt;.kickjs-assets.json&lt;/code&gt;. After the copy, &lt;code&gt;kick typegen&lt;/code&gt; regenerates &lt;code&gt;.kickjs/types/assets.d.ts&lt;/code&gt; so the type system reflects what was actually emitted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kick.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;assetMap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/templates/mails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.ejs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/schemas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, resolution follows a strict order. First, the manager checks &lt;code&gt;KICK_ASSETS_ROOT&lt;/code&gt; — if set, that directory is treated as the asset root and every lookup is rooted there. Second, it tries to load &lt;code&gt;.kickjs-assets.json&lt;/code&gt; from &lt;code&gt;dist/&lt;/code&gt; (or wherever the manager was initialized). Third — and only when running from source with no manifest present — it synthesizes a manifest by walking &lt;code&gt;src/&lt;/code&gt; according to &lt;code&gt;assetMap&lt;/code&gt;, so that &lt;code&gt;pnpm dev&lt;/code&gt; works without a build step.&lt;/p&gt;

&lt;p&gt;The contract is straightforward: in production you have a manifest and the proxy is essentially a hash lookup; in dev you have a synthesized one and the proxy is essentially a filesystem walk plus a cache. Same API, same types, different machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type safety via typegen
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kick typegen&lt;/code&gt; is the piece that closes the loop. It reads the manifest and writes a single &lt;code&gt;.d.ts&lt;/code&gt; that augments the &lt;code&gt;KickAssets&lt;/code&gt; interface declared by &lt;code&gt;@forinda/kickjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;KickAssets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;mails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;welcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AssetRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password-reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AssetRef&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AssetRef&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that file is on disk, &lt;code&gt;assets.mails.does_not_exist()&lt;/code&gt; is a compile error, not a runtime exception. Filenames that are not valid identifiers — anything with a hyphen, a leading digit, a dot — are still safely addressable through bracket notation: &lt;code&gt;assets.mails['password-reset']()&lt;/code&gt;. The generated interface uses string-literal keys for everything, so the bracket form keeps full autocomplete.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;kick typegen&lt;/code&gt; (or let &lt;code&gt;kick build&lt;/code&gt; do it for you) after any change to &lt;code&gt;assetMap&lt;/code&gt; or to the files those entries point at. CI should treat a stale &lt;code&gt;.kickjs/types/assets.d.ts&lt;/code&gt; as a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing — fixture swapping
&lt;/h2&gt;

&lt;p&gt;Because every lookup goes through the manifest, you can override the entire asset root at the process level by setting &lt;code&gt;KICK_ASSETS_ROOT&lt;/code&gt; before the manager is first touched. Combined with &lt;code&gt;clearAssetCache()&lt;/code&gt; to reset the manager between tests, this gives you a clean way to swap in fixtures without mocking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KICK_ASSETS_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;clearAssetCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a unit test for &lt;code&gt;MailService&lt;/code&gt;, you can point at a &lt;code&gt;fixtures/mails/welcome.ejs&lt;/code&gt; containing &lt;code&gt;&amp;lt;h1&amp;gt;Hello {{name}}&amp;lt;/h1&amp;gt;&lt;/code&gt; and assert on the rendered output without touching the real templates. The same trick works for snapshot fixtures, JSON-schema variants, anything where you want a known-good input.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;p&gt;The Asset Manager is the right tool whenever the runtime answer is &lt;code&gt;fs.readFile&lt;/code&gt;. Email templates rendered with EJS, Handlebars, or MJML. JSON schemas loaded by Ajv. Seed fixtures. PDFs and CSVs that ship with the app. Anything that lives on disk in dev, needs to live on disk in dist, and is consumed by your own server-side code.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; the right tool for HTTP-served static assets. Public images, downloadable user-facing files, anything a browser fetches — those want a static-file adapter or a CDN, not the Asset Manager. The line is "who reads this file?". If the answer is your handlers, use the manager. If the answer is a browser, use a serving layer.&lt;/p&gt;

&lt;p&gt;The payoff for the in-scope cases is that you stop writing path math, you stop scattering environment checks, and you find typos at compile time instead of at 2 a.m. Once the manifest exists, the type system does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/asset-manager.html" rel="noopener noreferrer"&gt;KickJS Asset Manager guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>build</category>
    </item>
    <item>
      <title>Dependency Injection in KickJS — Tokens, Scopes, and the Per-Request Factory</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:42:06 +0000</pubDate>
      <link>https://dev.to/forinda/dependency-injection-in-kickjs-tokens-scopes-and-the-per-request-factory-14il</link>
      <guid>https://dev.to/forinda/dependency-injection-in-kickjs-tokens-scopes-and-the-per-request-factory-14il</guid>
      <description>&lt;p&gt;Dependency injection in KickJS is small on purpose. There are four surfaces, two scopes, and one pattern — a per-request factory — that does most of the interesting work. Once you know how those pieces compose, you can wire anything from a process-wide HTTP client to a request-scoped derived value through the same container, and your services keep looking like the boring constructor-and-method classes you wanted them to be in the first place.&lt;/p&gt;

&lt;p&gt;This article walks the four DI entry points, explains when to use a token versus a class, draws the line between singleton and request scope, and then shows the one pattern that ties it together: an adapter that registers a request-scoped factory which reads request state and returns a derived handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four DI surfaces
&lt;/h2&gt;

&lt;p&gt;KickJS exposes DI through four distinct entry points. Each suits a different role, and a typical class will use two or three of them at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;@Service()&lt;/code&gt; — auto-registered class.&lt;/strong&gt; Decorate a class and the container learns to construct it. From inside any other &lt;code&gt;@Service()&lt;/code&gt; or &lt;code&gt;@Controller()&lt;/code&gt;, you can declare a property of that class type and the container fills it in. This is the "I wrote a class, I want it injectable" path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPricingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;Money&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;@Autowired(Class?)&lt;/code&gt; — property injection by class type.&lt;/strong&gt; Inside a service or controller, declare a property and the container fills it before the first method runs. With no argument it uses the property's type metadata; with a class argument it disambiguates explicitly. Use this when both ends of the wire are concrete classes you own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;@Inject(token)&lt;/code&gt; — constructor or property injection by token.&lt;/strong&gt; Use this when the type is an interface, when the implementation lives in another package, or when there are multiple implementations of the same shape and the choice is made by an adapter. Constructor form composes cleanly with classes that already have a constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAILER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailerService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;Container.registerInstance(token, value)&lt;/code&gt; / &lt;code&gt;Container.registerFactory(token, fn, scope)&lt;/code&gt; — manual registration.&lt;/strong&gt; This is the adapter's hook. When you have something the DI graph cannot construct on its own — a config value, a third-party client, a thing that needs async I/O at boot — you put it on the container imperatively. Use &lt;code&gt;registerInstance&lt;/code&gt; for an already-built value that lives forever; use &lt;code&gt;registerFactory&lt;/code&gt; when each resolve needs to compute a value (per-request derived state is the canonical case).&lt;/p&gt;

&lt;p&gt;The first three live in service files. The fourth lives in adapters. Mix them freely: a controller that is &lt;code&gt;@Service&lt;/code&gt;-decorated can &lt;code&gt;@Autowired&lt;/code&gt; another service and &lt;code&gt;@Inject&lt;/code&gt; a token in the same constructor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens vs classes
&lt;/h2&gt;

&lt;p&gt;Why bother with &lt;code&gt;createToken&amp;lt;MailerService&amp;gt;('MailerService')&lt;/code&gt; when you could &lt;code&gt;@Autowired(MailerService)&lt;/code&gt; directly? Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript has no nominal typing.&lt;/strong&gt; Two interfaces with the same shape are the same interface. If you want DI to bind to "the mailer," not to "anything that happens to have &lt;code&gt;send(message)&lt;/code&gt;," you need an explicit identity. &lt;code&gt;createToken&lt;/code&gt; returns a frozen object that is identity-keyed — that is the whole point of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-package interfaces.&lt;/strong&gt; Implementations often live in adapter packages; consumers often live in domain or use-case packages. The use-case should not import the concrete class. It should import the &lt;em&gt;interface&lt;/em&gt; (or a type-only re-export) plus the token. The token is the only runtime value crossing the boundary; everything else is a type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple implementations behind one interface.&lt;/strong&gt; When you have a &lt;code&gt;ConsoleProvider&lt;/code&gt; for dev and a real provider for prod, &lt;code&gt;@Inject(MAILER)&lt;/code&gt; always resolves to whichever the adapter registered. The use-case never names a class, and swapping providers is a one-line change at the composition root.&lt;/p&gt;

&lt;p&gt;Rule of thumb: classes for use-cases composing other use-cases inside the same app; tokens for anything that crosses an adapter boundary, has multiple implementations, or is interface-typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Singleton vs Scope.REQUEST
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;registerInstance&lt;/code&gt; is implicitly singleton — one value, lives until shutdown. &lt;code&gt;registerFactory&lt;/code&gt; defaults to singleton too (the factory runs once, the result is cached), but pass &lt;code&gt;Scope.REQUEST&lt;/code&gt; and the factory runs &lt;em&gt;every request&lt;/em&gt;, with the result cached for the duration of that request frame.&lt;/p&gt;

&lt;p&gt;When does each fit?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Singleton — app-level handles.&lt;/strong&gt; A database pool, a mailer, a secrets provider, an HTTP client, a feature-flag SDK. Anything you build once, reuse forever, and tear down on shutdown. These have no per-request variance; they are shared, expensive to build, and rebuilding them between requests would be wasteful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope.REQUEST — per-request derived state.&lt;/strong&gt; Anything whose value depends on &lt;em&gt;which&lt;/em&gt; request is in flight. A current-user object derived from the token. A request-scoped logger that prefixes log lines with the request ID. A correlation-context object. A "which one of these N pooled resources does this request want" selector. The factory runs once per request, the result is reused for that request's lifetime, and nothing leaks between requests.&lt;/p&gt;

&lt;p&gt;The win of &lt;code&gt;Scope.REQUEST&lt;/code&gt; over "stash it on the request object and pass it around" is that injection points stay clean. A use-case ten levels deep can &lt;code&gt;@Autowired&lt;/code&gt; a request-scoped token and get the right value; no signature in the call chain has to thread request state through. The framework keeps a per-request DI scope alive between the request entering the kernel and the response being flushed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapter-side registration
&lt;/h2&gt;

&lt;p&gt;Adapters are KickJS's lifecycle primitive. They expose hooks (&lt;code&gt;beforeStart&lt;/code&gt;, &lt;code&gt;middleware&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;) that the kernel calls in a defined order. &lt;code&gt;beforeStart({ container })&lt;/code&gt; is where you populate the DI graph. The contract: &lt;code&gt;beforeStart&lt;/code&gt; runs &lt;em&gt;after&lt;/em&gt; every module's &lt;code&gt;register()&lt;/code&gt; (so all &lt;code&gt;@Service&lt;/code&gt; classes are known) but &lt;em&gt;before&lt;/em&gt; the HTTP server listens (so the first request resolves a fully-wired graph).&lt;/p&gt;

&lt;p&gt;The simplest adapter just registers a singleton instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailerAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailerConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailerAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAILER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailerService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice. The adapter receives its config at &lt;em&gt;factory invocation&lt;/em&gt; time, so the composition root decides whether dev gets a console provider and prod gets a real one. The only runtime side effect is the &lt;code&gt;registerInstance&lt;/code&gt; call. And no use-case imports the concrete class — &lt;code&gt;@Inject(MAILER)&lt;/code&gt; is the entire contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The per-request factory pattern
&lt;/h2&gt;

&lt;p&gt;Now the centerpiece. The pattern shows up any time you have a process-wide pool of resources and a per-request rule for picking one of them. A few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A multi-tenant app with one DB client per tenant, where the active tenant is resolved from the request.&lt;/li&gt;
&lt;li&gt;A feature-flag SDK that needs to evaluate flags against the current user, where "current user" is per-request.&lt;/li&gt;
&lt;li&gt;A logger that prefixes structured fields drawn from request context.&lt;/li&gt;
&lt;li&gt;A unit-of-work / transaction scope that should be the same instance for the whole request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape is always the same: one process-wide registry (singleton), one factory that reads request state and returns the right slice of it (&lt;code&gt;Scope.REQUEST&lt;/code&gt;), one token that injection sites bind to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REQUEST_DB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DbHandle&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RequestDb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// inside an adapter's beforeStart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dbRegistry&lt;/span&gt; &lt;span class="c1"&gt;// singleton: a Map&amp;lt;key, DbHandle&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;REQUEST_DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ctx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REQUEST_DB used outside a request scope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three moving parts deserve attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The registry is a singleton.&lt;/strong&gt; It is built once at boot and lives until shutdown. It might cache promises so concurrent first-time resolves share the same open. It typically exposes a &lt;code&gt;closeAll()&lt;/code&gt; for the adapter's &lt;code&gt;shutdown&lt;/code&gt; hook. The pool itself is shared; what is request-scoped is the &lt;em&gt;selection&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The factory closure reads request state.&lt;/strong&gt; &lt;code&gt;getRequestValue('ctx')&lt;/code&gt; (or however the framework exposes per-request key/value storage) is populated upstream — by middleware, a contributor, or a guard that runs before any handler. By the time a use-case resolves the token, the value is already there, the registry hits its cache, and the factory returns the same handle every other in-flight request for the same key is using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failing loudly outside a request.&lt;/strong&gt; Calling the token outside a request frame — a background worker, a CLI script, a test that forgot to set up the scope — has no request state to read. Returning &lt;code&gt;undefined&lt;/code&gt; would push the failure into a confusing null-pointer crash deep inside a use-case. Throwing at resolve time makes the bug unmistakable: "you used this from outside a request scope." Background callers that legitimately need the resource have to either run inside an explicit request scope or call the registry directly. Either is fine; neither is a silent no-op.&lt;/p&gt;

&lt;p&gt;The injection site stays oblivious to all of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateOrderUseCase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Autowired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REQUEST_DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DbHandle&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateOrderDTO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No resolver call, no plumbing of context through the signature, no reaching for global state. Just an injected handle that happens to be the right one for this request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test seam
&lt;/h2&gt;

&lt;p&gt;The same surface gives you a clean test seam. In a test adapter, register an instance instead of a factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REQUEST_DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fakeHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;registerInstance&lt;/code&gt; overrides whatever was registered before, so the stub wins over the real factory. Tests get a deterministic in-memory handle without spinning up infrastructure or mocking the request-state accessor. The use-case under test is none the wiser — same token, different binding, different scope (singleton in tests, request in production), and the production code does not change.&lt;/p&gt;

&lt;p&gt;This is the payoff of token-based DI: the contract between caller and provider is one frozen object reference, and you can swap providers along any axis — environment, scope, mock vs real — without touching either side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;If you remember three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the right surface for the right job.&lt;/strong&gt; &lt;code&gt;@Service&lt;/code&gt; and &lt;code&gt;@Autowired&lt;/code&gt; for in-app composition; &lt;code&gt;@Inject(token)&lt;/code&gt; for cross-boundary interfaces and pluggable implementations; &lt;code&gt;registerInstance&lt;/code&gt; / &lt;code&gt;registerFactory&lt;/code&gt; for adapters bringing in things the graph cannot build itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match scope to variance.&lt;/strong&gt; Singleton for things that do not vary per request. &lt;code&gt;Scope.REQUEST&lt;/code&gt; for things that do. The factory closure is your bridge between the two scopes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep injection sites scope-agnostic.&lt;/strong&gt; A use-case asking for a request-scoped token should look identical to one asking for a singleton. The token is the contract; everything else is the adapter's problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once those three habits stick, DI stops being a topic you think about and becomes the boring plumbing it is supposed to be — which is exactly when it earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/" rel="noopener noreferrer"&gt;KickJS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjsl</category>
      <category>typescript</category>
      <category>dependency</category>
      <category>multitenant</category>
    </item>
    <item>
      <title>Type-Safe Augmentation Patterns in KickJS — ContextMeta, AuthUser, PolicyRegistry</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:39:10 +0000</pubDate>
      <link>https://dev.to/forinda/type-safe-augmentation-patterns-in-kickjs-contextmeta-authuser-policyregistry-27mk</link>
      <guid>https://dev.to/forinda/type-safe-augmentation-patterns-in-kickjs-contextmeta-authuser-policyregistry-27mk</guid>
      <description>&lt;p&gt;Decorator decorations are unsound until you augment. &lt;code&gt;@Roles('owner', 'admin')&lt;/code&gt; &lt;em&gt;looks&lt;/em&gt; like it is type-checked — it is not, until you tell TypeScript what &lt;code&gt;AuthUser['roles']&lt;/code&gt; actually is. &lt;code&gt;ctx.get('tenant')&lt;/code&gt; looks like it returns a tenant — it does not, until you tell &lt;code&gt;ContextMeta&lt;/code&gt; that &lt;code&gt;'tenant'&lt;/code&gt; is a key. &lt;code&gt;@Can('delete', 'invoice')&lt;/code&gt; looks like a permission check — but until &lt;code&gt;PolicyRegistry&lt;/code&gt; knows about &lt;code&gt;'invoice'&lt;/code&gt;, both arguments are just &lt;code&gt;string&lt;/code&gt;. KickJS v5 ships these three interfaces deliberately empty so each adopter can fill them in, and the framework's helper types pivot on whether you have. This article walks the three augmentation surfaces, the catalogue-only &lt;code&gt;defineAugmentation()&lt;/code&gt; helper, and the file-organisation rules — generically, with minimal examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general pattern
&lt;/h2&gt;

&lt;p&gt;Every augmentation in KickJS uses the same TypeScript primitive — module declaration merging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* your keys */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthUser&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* your shape */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PolicyRegistry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* your resources */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are reaching into the framework's exported &lt;code&gt;interface&lt;/code&gt;, adding members from your project, and the compiler stitches the result into a single shape at every consumption site. The framework ships each interface empty (or with a permissive index signature) so unaugmented projects continue to compile against the loose fallback; once you augment, every helper type that pivots on &lt;code&gt;keyof ContextMeta&lt;/code&gt; or &lt;code&gt;AuthUser['roles'][number]&lt;/code&gt; automatically tightens.&lt;/p&gt;

&lt;p&gt;A few mechanical rules — get these wrong and the augmentation silently does nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The file must be a &lt;strong&gt;module&lt;/strong&gt;. Add &lt;code&gt;export {}&lt;/code&gt; at the bottom if you do not have any other top-level imports/exports. A plain script with &lt;code&gt;declare module&lt;/code&gt; blocks is treated as ambient and the merge does not always land where you expect.&lt;/li&gt;
&lt;li&gt;The module specifier inside &lt;code&gt;declare module '...'&lt;/code&gt; must match the &lt;strong&gt;published package name&lt;/strong&gt; verbatim — &lt;code&gt;'@forinda/kickjs'&lt;/code&gt; and &lt;code&gt;'@forinda/kickjs-auth'&lt;/code&gt; are different modules, so &lt;code&gt;ContextMeta&lt;/code&gt; lives in the first, &lt;code&gt;AuthUser&lt;/code&gt; and &lt;code&gt;PolicyRegistry&lt;/code&gt; in the second.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tsconfig.json&lt;/code&gt; must include the &lt;code&gt;.d.ts&lt;/code&gt; file. If you put augmentations in &lt;code&gt;src/types/&lt;/code&gt; and your &lt;code&gt;include&lt;/code&gt; is &lt;code&gt;["src/**/*"]&lt;/code&gt;, you are fine. If you carved out a narrower glob, double-check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The convention this article recommends is one file per interface, all under a dedicated &lt;code&gt;src/types/&lt;/code&gt; folder. The reasoning is in "Where to put augmentation files" below.&lt;/p&gt;

&lt;h2&gt;
  
  
  ContextMeta — typed &lt;code&gt;ctx.get()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The framework declares &lt;code&gt;ContextMeta&lt;/code&gt; as an empty marker interface, paired with a &lt;code&gt;MetaValue&amp;lt;K, Fallback&amp;gt;&lt;/code&gt; helper that resolves to &lt;code&gt;ContextMeta[K]&lt;/code&gt; if the key is registered, and to &lt;code&gt;Fallback&lt;/code&gt; (default &lt;code&gt;unknown&lt;/code&gt;) otherwise. &lt;code&gt;ctx.get&amp;lt;K extends string&amp;gt;(key)&lt;/code&gt; then dispatches through &lt;code&gt;MetaValue&lt;/code&gt;. With nothing augmented, every &lt;code&gt;ctx.get('anything')&lt;/code&gt; resolves to &lt;code&gt;unknown | undefined&lt;/code&gt; — strict, but it does not break the call site.&lt;/p&gt;

&lt;p&gt;Once you augment, the same call narrows. A typical &lt;code&gt;src/types/context-meta.d.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatedUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/auth/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tenant&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/tenancy/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthenticatedUser&lt;/span&gt;
    &lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tenant&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;ctx.get('tenant')&lt;/code&gt; is &lt;code&gt;Tenant | undefined&lt;/code&gt; and &lt;code&gt;ctx.get('user')&lt;/code&gt; is &lt;code&gt;AuthenticatedUser | undefined&lt;/code&gt;. Two keys, two narrowings, zero runtime cost.&lt;/p&gt;

&lt;p&gt;The contributors that &lt;strong&gt;populate&lt;/strong&gt; those keys are a separate mechanism — typically &lt;code&gt;defineHttpContextDecorator({ key: 'tenant', resolve: ... })&lt;/code&gt; registered in your adapter layer. The relationship is worth saying out loud because beginners trip on it: the augmentation declares &lt;strong&gt;types&lt;/strong&gt;, the contributor declares &lt;strong&gt;values&lt;/strong&gt;. They are tied by the literal &lt;code&gt;key&lt;/code&gt; string. If you augment &lt;code&gt;ContextMeta&lt;/code&gt; with &lt;code&gt;tenant: Tenant&lt;/code&gt; but never register a contributor that resolves &lt;code&gt;'tenant'&lt;/code&gt; for the route, the framework throws a &lt;code&gt;MissingContributorError&lt;/code&gt; at boot when it builds the per-route pipeline. That is the system catching a typo (or a forgotten registration) before the first request hits production.&lt;/p&gt;

&lt;p&gt;A common sub-pattern is marking certain contributors &lt;code&gt;optional: true&lt;/code&gt; so they can no-op on routes where the value is genuinely absent (health checks, public webhooks). The &lt;code&gt;| undefined&lt;/code&gt; already in &lt;code&gt;ctx.get&lt;/code&gt;'s return type accommodates this without callers needing extra ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  AuthUser['roles'] — typed &lt;code&gt;@Roles&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Roles&lt;/code&gt; is the most common decorator that benefits from augmentation, and the framework's machinery for it is the cleanest demonstration of why module merging beats subclassing or generics.&lt;/p&gt;

&lt;p&gt;The framework's &lt;code&gt;Role&lt;/code&gt; helper does roughly this: it looks at &lt;code&gt;AuthUser['roles']&lt;/code&gt;, and if the type is the literal &lt;code&gt;any&lt;/code&gt; (which it will be when &lt;code&gt;AuthUser&lt;/code&gt; is unaugmented and falls back to its index signature), it widens &lt;code&gt;Role&lt;/code&gt; to &lt;code&gt;string&lt;/code&gt;; otherwise, when &lt;code&gt;roles&lt;/code&gt; has been narrowed to a tuple or array of literal strings, it pulls the element type out and uses that. &lt;code&gt;@Roles&amp;lt;R extends Role&amp;gt;(...roles: R[])&lt;/code&gt; then keys off the result.&lt;/p&gt;

&lt;p&gt;The reason that &lt;code&gt;any&lt;/code&gt; check is load-bearing: &lt;code&gt;any&lt;/code&gt; matches every conditional branch, so without an explicit pivot the helper would collapse to &lt;code&gt;Role = any&lt;/code&gt; and &lt;code&gt;@Roles(...)&lt;/code&gt; would silently accept anything — numbers, objects, undefined. The pivot keeps the unaugmented case loose-but-sane (&lt;code&gt;Role = string&lt;/code&gt;) and lets the augmented case tighten into a proper literal union.&lt;/p&gt;

&lt;p&gt;A minimal augmentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthUser&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, &lt;code&gt;@Roles('owner', 'admin')&lt;/code&gt; typechecks and &lt;code&gt;@Roles('admni')&lt;/code&gt; is a red squiggle at the decoration site, not a 403 in production.&lt;/p&gt;

&lt;p&gt;There is a second layer worth noting — defense in depth. The augmentation only narrows what the &lt;strong&gt;TypeScript compiler&lt;/strong&gt; can see. A forged JWT with &lt;code&gt;"roles": ["god-mode"]&lt;/code&gt; could still arrive at runtime, and if your &lt;code&gt;mapPayload&lt;/code&gt; returned that payload as-is, downstream &lt;code&gt;@Roles&lt;/code&gt; checks would compare against &lt;code&gt;'god-mode'&lt;/code&gt; and reject the request, but the user object would carry an off-union string. The fix is to filter explicitly in &lt;code&gt;mapPayload&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;is &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So bogus roles are scrubbed before they ever land in the &lt;code&gt;AuthUser&lt;/code&gt; shape. The compile-time augmentation gives you authoring safety; the &lt;code&gt;mapPayload&lt;/code&gt; filter gives you runtime safety. The two together mean every code path that reads &lt;code&gt;user.roles&lt;/code&gt; — &lt;code&gt;@Roles&lt;/code&gt;, your authorization service, your service-layer ABAC checks — sees the same closed union.&lt;/p&gt;

&lt;h2&gt;
  
  
  PolicyRegistry — typed &lt;code&gt;@Can&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Can('action', 'resource')&lt;/code&gt; follows the same pattern, with two indices instead of one. The framework declares &lt;code&gt;PolicyRegistry&lt;/code&gt; empty, then derives &lt;code&gt;PolicyResource&lt;/code&gt; and &lt;code&gt;PolicyAction&amp;lt;R&amp;gt;&lt;/code&gt; from it. The trick is that when the registry has no keys at all, both helpers fall back to &lt;code&gt;string&lt;/code&gt; so unaugmented projects keep compiling; the moment you add keys, the resource argument narrows to your declared keys and the action argument narrows &lt;em&gt;per-resource&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PolicyRegistry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;void&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;void&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// ✓&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// ✓&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// ✗ 'typo' not in invoice's actions&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// ✗ 'invite' is a 'user' action&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// ✗ 'unknown' is not a registered resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The augmentation file becomes the single source of truth: every time a new resource gains an action, you update the registry once and every existing &lt;code&gt;@Can&lt;/code&gt; call site is re-checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;defineAugmentation()&lt;/code&gt; catch
&lt;/h2&gt;

&lt;p&gt;There is one extra primitive that confuses everyone the first time they see it. KickJS exports a &lt;code&gt;defineAugmentation('InterfaceName', { description, example })&lt;/code&gt; helper, and the temptation is to assume calling it &lt;em&gt;is&lt;/em&gt; the augmentation. It is not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defineAugmentation&lt;/code&gt; is purely a discovery aid — it is a no-op at runtime and a no-op at the type level. All it does is add an entry to a typegen catalogue so the &lt;code&gt;kick typegen&lt;/code&gt; command can render a single &lt;code&gt;.kickjs/types/augmentations.d.ts&lt;/code&gt; file listing every augmentable interface in the project plus a worked example. A new contributor can &lt;code&gt;grep&lt;/code&gt; that one file and see the surface area at a glance.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;nothing&lt;/strong&gt; to the type system. If you call only &lt;code&gt;defineAugmentation&lt;/code&gt; and skip the &lt;code&gt;declare module&lt;/code&gt; block, your &lt;code&gt;ctx.get('tenant')&lt;/code&gt; is still &lt;code&gt;unknown&lt;/code&gt;. Conversely, if you have the &lt;code&gt;declare module&lt;/code&gt; block and skip &lt;code&gt;defineAugmentation&lt;/code&gt;, everything works perfectly — you just lose the catalogue entry. The two are independent. Treat &lt;code&gt;defineAugmentation&lt;/code&gt; as documentation that your linter can find.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to put augmentation files
&lt;/h2&gt;

&lt;p&gt;A few file-organisation rules that save grief:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One block per interface, one file per block.&lt;/strong&gt; TypeScript will happily merge multiple &lt;code&gt;declare module&lt;/code&gt; blocks for the same interface across the project, but tools (IDE go-to-definition, code search, refactor tooling) work better when each augmentable interface has exactly one home. A typical layout: &lt;code&gt;src/types/context-meta.d.ts&lt;/code&gt;, &lt;code&gt;src/types/auth-augment.d.ts&lt;/code&gt;, &lt;code&gt;src/types/policy-augment.d.ts&lt;/code&gt;. When a new resource appears, you know exactly which file to open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always end with &lt;code&gt;export {}&lt;/code&gt;.&lt;/strong&gt; Without it the file is a script, and ambient script semantics differ from module semantics in ways that bite you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import the value types as &lt;code&gt;type&lt;/code&gt;-only.&lt;/strong&gt; &lt;code&gt;import type { Tenant } from '@/tenancy/types'&lt;/code&gt; keeps the augmentation file out of the runtime graph — &lt;code&gt;.d.ts&lt;/code&gt; files have no runtime, but a stray value-import pulls the source file into the IDE's "go to" results and clutters refactor scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm &lt;code&gt;tsconfig.include&lt;/code&gt; covers `src/types/&lt;/strong&gt;&lt;code&gt;.** A &lt;/code&gt;.d.ts` outside the project's compilation graph does nothing, and TypeScript will not warn you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;The whole augmentation surface is small — three interfaces, three files — but the leverage is enormous. Every decorator, every &lt;code&gt;ctx.get(key)&lt;/code&gt;, every policy call site becomes compile-checked. When a role or resource changes in the source-of-truth file, every consumer is forced to acknowledge it. Drift between "what the auth provider hands me" and "what the decorator thinks is allowed" stops being a runtime mystery and becomes a compile error in the same PR that introduced it.&lt;/p&gt;

&lt;p&gt;The pattern also composes. New surfaces — a typed feature-flag registry, a typed event bus, a typed job queue — fit the same shape: a framework-side empty interface, an adopter-side &lt;code&gt;declare module&lt;/code&gt; block, a helper type pivoting on &lt;code&gt;keyof&lt;/code&gt; or &lt;code&gt;extends never&lt;/code&gt;, and an optional catalogue-only &lt;code&gt;defineAugmentation&lt;/code&gt; call. That is what "type-safe by construction" looks like in v5.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/authorization.html" rel="noopener noreferrer"&gt;KickJS authorization guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/context-decorators.html" rel="noopener noreferrer"&gt;KickJS context-decorators guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>types</category>
      <category>augmentation</category>
    </item>
    <item>
      <title>The KickJS CLI Toolbox — Generators, Typegen, and the Configurable Build</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:37:09 +0000</pubDate>
      <link>https://dev.to/forinda/the-kickjs-cli-toolbox-generators-typegen-and-the-configurable-build-56hp</link>
      <guid>https://dev.to/forinda/the-kickjs-cli-toolbox-generators-typegen-and-the-configurable-build-56hp</guid>
      <description>&lt;p&gt;Every framework ships a CLI. Most of them are scaffold-and-forget: a one-time &lt;code&gt;nest new&lt;/code&gt; or &lt;code&gt;next create&lt;/code&gt;, then you forget the binary exists and live inside &lt;code&gt;package.json&lt;/code&gt; scripts forever. KickJS's &lt;code&gt;kick&lt;/code&gt; CLI sits in a different bucket. It is the build tool, the codegen, the diagnostics surface, the package wirer, and — unusually — an interactive REPL with your DI container loaded.&lt;/p&gt;

&lt;p&gt;This is not an enumeration of every flag. It is a tour grouped by intent, with the less-obvious commands called out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle — the build tool that happens to ship with the framework
&lt;/h2&gt;

&lt;p&gt;The lifecycle commands are what most teams wire into &lt;code&gt;package.json&lt;/code&gt; and forget about, but they are doing more than &lt;code&gt;tsc --watch&lt;/code&gt; would.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kick new &amp;lt;name&amp;gt;&lt;/code&gt; and &lt;code&gt;kick init&lt;/code&gt; create a new project. Pass &lt;code&gt;.&lt;/code&gt; to &lt;code&gt;init&lt;/code&gt; to scaffold into the current directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick dev&lt;/code&gt; is a Vite-backed dev server with HMR. It runs typegen on startup, watches &lt;code&gt;src/**/*.ts&lt;/code&gt; and &lt;code&gt;kick.config.ts&lt;/code&gt;, and tears down adapters that implement &lt;code&gt;shutdown()&lt;/code&gt; cleanly between reloads.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick dev:debug&lt;/code&gt; is the same dev server with the Node.js inspector attached, so you can hook a debugger without remembering the &lt;code&gt;--inspect&lt;/code&gt; flag pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick build&lt;/code&gt; runs the production Vite build and emits a single ESM bundle in &lt;code&gt;dist/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick build:assets&lt;/code&gt; rebuilds only the &lt;code&gt;.kickjs-assets.json&lt;/code&gt; manifest — useful when only static assets changed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick start&lt;/code&gt; runs the compiled production build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These share state through generated artefacts. &lt;code&gt;kick dev&lt;/code&gt; regenerates types as you type; &lt;code&gt;kick build&lt;/code&gt; regenerates them before bundling; &lt;code&gt;kick start&lt;/code&gt; assumes the build was clean. You don't think about it, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generators — &lt;code&gt;kick g&lt;/code&gt; is the codegen surface
&lt;/h2&gt;

&lt;p&gt;The bare form &lt;code&gt;kick g &amp;lt;name&amp;gt;&lt;/code&gt; is shorthand for &lt;code&gt;kick g module &amp;lt;name&amp;gt;&lt;/code&gt;, because that is what you reach for nine times out of ten. The full set of subcommands covers the whole granularity ladder.&lt;/p&gt;

&lt;p&gt;Full-feature generators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;module&lt;/code&gt; — a complete module folder: controller, service, DTOs, repo, tests, and (critically) registration in &lt;code&gt;src/modules/index.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scaffold&lt;/code&gt; — full CRUD module driven by a field DSL (more on this in a moment).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth-scaffold&lt;/code&gt; — a complete auth module: register, login, logout, password hashing, controller, service, DTOs, test stubs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;plugin&lt;/code&gt; — a &lt;code&gt;KickPlugin&lt;/code&gt; that bundles DI bindings, modules, adapters, and middleware behind a single registration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adapter&lt;/code&gt; — an &lt;code&gt;AppAdapter&lt;/code&gt; with lifecycle hooks (&lt;code&gt;start&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Single-file scaffolds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;service&lt;/code&gt;, &lt;code&gt;controller&lt;/code&gt;, &lt;code&gt;dto&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt; — each takes &lt;code&gt;-m &amp;lt;module&amp;gt;&lt;/code&gt; to scope it to an existing module folder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;middleware&lt;/code&gt; — Express middleware (also &lt;code&gt;-m&lt;/code&gt;-scopable).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;guard&lt;/code&gt; — a route guard (auth, roles, anything per-route).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;job&lt;/code&gt; — a &lt;code&gt;@Job&lt;/code&gt; queue processor with &lt;code&gt;@Process&lt;/code&gt; handlers, for &lt;code&gt;kick add queue&lt;/code&gt; consumers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolver&lt;/code&gt; — a GraphQL resolver scaffold with the right decorators wired up. Opt-in: only relevant if you're running a GraphQL surface alongside (or instead of) HTTP routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Project-level generators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;config&lt;/code&gt; — emit a fresh &lt;code&gt;kick.config.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agents&lt;/code&gt; and &lt;code&gt;agent-docs&lt;/code&gt; — regenerate &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, and &lt;code&gt;kickjs-skills.md&lt;/code&gt; from the framework's current state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cross-cutting flags worth knowing: &lt;code&gt;--list&lt;/code&gt; shows what a generator can do without writing anything, &lt;code&gt;--dry-run&lt;/code&gt; prints the file plan, &lt;code&gt;--repo (inmemory | drizzle | prisma)&lt;/code&gt; picks the persistence flavour, &lt;code&gt;--pattern (rest | ddd | cqrs | minimal)&lt;/code&gt; picks the architectural dialect, &lt;code&gt;--minimal&lt;/code&gt; / &lt;code&gt;--no-entity&lt;/code&gt; / &lt;code&gt;--no-tests&lt;/code&gt; / &lt;code&gt;--no-pluralize&lt;/code&gt; strip pieces you don't want, and &lt;code&gt;-f&lt;/code&gt; / &lt;code&gt;--force&lt;/code&gt; overwrites existing files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kick g module orders &lt;span class="nt"&gt;--repo&lt;/span&gt; drizzle &lt;span class="nt"&gt;--pattern&lt;/span&gt; rest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers &lt;code&gt;OrdersModule&lt;/code&gt; in &lt;code&gt;src/modules/index.ts&lt;/code&gt; for you. The registration step is the one you must not hand-roll: it is alphabetised, import-managed, and aware of removal via &lt;code&gt;kick rm&lt;/code&gt;. Hand-edit it and you will eventually leave a dangling import behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unusual ones, paragraph by paragraph
&lt;/h2&gt;

&lt;p&gt;These are the commands that most other frameworks don't have, or only have in some half-built form. They're the reason the CLI is interesting and not just convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick g scaffold&lt;/code&gt; and the field DSL
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kick g scaffold&lt;/code&gt; takes a list of field specs and emits a full CRUD: Zod input DTO, response DTO, controller (list/get/create/update/archive), use-case files, and tests. The DSL is the trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kick g scaffold Post title:string body:text:optional published:boolean:optional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported types: &lt;code&gt;string, text, number, int, float, boolean, date, email, url, uuid, json&lt;/code&gt; and &lt;code&gt;enum:a,b,c&lt;/code&gt; for inline enums. Append &lt;code&gt;:optional&lt;/code&gt; and the Zod schema marks it nullable, the TypeScript type widens, and the test fixtures stop passing it. The right tool when you know the shape of an entity and don't want to type it four times across DTO, response, repo, and test.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick g auth-scaffold&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kick g auth-scaffold&lt;/code&gt; is &lt;code&gt;g scaffold&lt;/code&gt; for the one feature every app re-implements badly: authentication. It emits a controller (&lt;code&gt;/auth/register&lt;/code&gt;, &lt;code&gt;/auth/login&lt;/code&gt;, &lt;code&gt;/auth/logout&lt;/code&gt;), a service with password hashing, the DTOs you actually want, and test stubs. The point isn't that it saves typing; it's that the resulting code is the same shape across every KickJS app, so onboarding onto a new project, you already know where the login handler lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick tinker&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kick tinker&lt;/code&gt; boots an interactive REPL with the DI container and your services pre-loaded — the Laravel/Rails ergonomic, ported into Node. Drop into a prompt, type &lt;code&gt;await container.resolve(OrdersService).list({ limit: 5 })&lt;/code&gt;, and you have data back without writing a script, mocking a request, or curl-ing a route. For exploratory work — "what does this query return for tenant X?" — it beats a debugger.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick explain&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kick explain "&amp;lt;error message&amp;gt;"&lt;/code&gt; takes a KickJS error string and returns a description plus a likely fix from the framework's own catalogue. You're not searching GitHub issues; you're asking the binary that produced the error what it meant. A small thing that's disproportionately useful the first month on a new framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick mcp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kick mcp start&lt;/code&gt; and &lt;code&gt;kick mcp init&lt;/code&gt; expose the framework over the Model Context Protocol — the spec that lets AI assistants treat tools as first-class resources. With &lt;code&gt;kick mcp&lt;/code&gt; running, an assistant can introspect your modules, ask the framework for the DI registry, or trigger generators safely. The first framework I've seen ship its own MCP server in the box — a clear opinion that AI tooling is part of the developer surface, not a third-party add-on.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;kick g agents&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The companion to &lt;code&gt;kick mcp&lt;/code&gt; is &lt;code&gt;kick g agents&lt;/code&gt;, which regenerates &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, and &lt;code&gt;kickjs-skills.md&lt;/code&gt; from the framework's current capabilities. The interesting bit is what it implies: the framework treats keeping the AI-tooling docs fresh as part of its own upgrade process. Bump the framework, re-run &lt;code&gt;kick g agents&lt;/code&gt;, and your AI tooling is back in sync. The doc isn't a side artefact; it's a build output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages — &lt;code&gt;kick add&lt;/code&gt; is not a synonym for &lt;code&gt;pnpm install&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kick add auth&lt;/code&gt; does what &lt;code&gt;pnpm install @forinda/kickjs-auth&lt;/code&gt; does, &lt;strong&gt;and&lt;/strong&gt; wires it. It registers the package's adapters with the bootstrapper, drops imports into &lt;code&gt;src/index.ts&lt;/code&gt; where needed, and scaffolds the config block (JWT secret slot, refresh strategy slot) so you have somewhere to put secrets without reading the package README first. The package ships a manifest, the CLI applies the wiring, and prints what it changed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kick list&lt;/code&gt; (alias &lt;code&gt;kick ls&lt;/code&gt;) prints every package the CLI knows how to wire — the answer to "is there an official kickjs package for cron?" without leaving the terminal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick remove&lt;/code&gt; (alias &lt;code&gt;kick rm&lt;/code&gt;) removes generated code with awareness of the registry files. &lt;code&gt;kick rm module orders&lt;/code&gt; undoes what &lt;code&gt;kick g module orders&lt;/code&gt; did, including the &lt;code&gt;modules/index.ts&lt;/code&gt; line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Diagnostics &amp;amp; DX — the bits other CLIs ship as third-party tools
&lt;/h2&gt;

&lt;p&gt;None of these are scripts wrapping &lt;code&gt;npm run&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kick info&lt;/code&gt; prints system + framework info: Node version, TS version, KickJS version, package versions, OS. The first thing you paste into a bug report.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick check&lt;/code&gt; audits the project for common issues — missing types, drift between &lt;code&gt;kick.config.ts&lt;/code&gt; and the generated registry, packages added via &lt;code&gt;pnpm&lt;/code&gt; without the &lt;code&gt;kick add&lt;/code&gt; wiring step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick inspect [url]&lt;/code&gt; connects to a running KickJS app and pulls live debug info: registered routes, DI bindings, adapter status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cumulatively, you can answer "what's wrong with my app?" without leaving the CLI — novel only if you remember how many frameworks make you write a script, run a curl, or boot an inspector to ask the same questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality — the CI gate
&lt;/h2&gt;

&lt;p&gt;The quality commands are small in scope but hold the project together over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kick typegen&lt;/code&gt; regenerates &lt;code&gt;.kickjs/types/&lt;/code&gt;: route types (so &lt;code&gt;ctx.body&lt;/code&gt; matches your Zod schema), env types (so &lt;code&gt;getEnv('PORT')&lt;/code&gt; is typed), DI registry tokens, module + plugin declarations, asset imports, ContextMeta augmentations. &lt;code&gt;gitignore&lt;/code&gt;d, treated as a build artefact. &lt;code&gt;kick dev&lt;/code&gt; runs it on change; CI runs it before typecheck and build. You rarely run it by hand.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick test&lt;/code&gt; is a passthrough to Vitest with the project's config preloaded.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick format&lt;/code&gt; runs Prettier; &lt;code&gt;kick format:check&lt;/code&gt; checks without writing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kick ci:check&lt;/code&gt; (alias &lt;code&gt;kick verify&lt;/code&gt;) is the CI gate: typecheck + format check, in the order that fails fastest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal synthetic &lt;code&gt;kick.config.ts&lt;/code&gt; ties it together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs-cli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drizzle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pluralize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;typegen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schemaValidator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lint code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eslint src/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pattern&lt;/code&gt; picks the controller dialect. &lt;code&gt;modules.dir&lt;/code&gt; / &lt;code&gt;modules.repo&lt;/code&gt; / &lt;code&gt;modules.pluralize&lt;/code&gt; set generator defaults. &lt;code&gt;typegen.schemaValidator: 'zod'&lt;/code&gt; tells typegen to crack open Zod schemas for body inference. &lt;code&gt;commands&lt;/code&gt; lets you register custom verbs that run as &lt;code&gt;kick lint&lt;/code&gt;, with the same logging the built-ins get.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this CLI different
&lt;/h2&gt;

&lt;p&gt;KickJS folds five things — build tool, codegen, package manager wrapper, diagnostics, REPL — into one binary, and ships an MCP server so AI assistants can introspect the app. Most Node frameworks ship one or two of those and rely on you to assemble the rest from &lt;code&gt;package.json&lt;/code&gt; scripts and third-party tools. The design decision is worth stealing: a single CLI that stays useful from &lt;code&gt;kick new&lt;/code&gt; through to &lt;code&gt;kick tinker&lt;/code&gt; on a five-year-old codebase is the one you'll still trust on day 1500.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/cli-commands.html" rel="noopener noreferrer"&gt;KickJS CLI commands docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs-cli" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs-cli&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>cli</category>
    </item>
    <item>
      <title>BYO in KickJS v5 — Building Your Own Mailer (and Why That's a Good Thing)</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:35:07 +0000</pubDate>
      <link>https://dev.to/forinda/byo-in-kickjs-v5-building-your-own-mailer-and-why-thats-a-good-thing-ddo</link>
      <guid>https://dev.to/forinda/byo-in-kickjs-v5-building-your-own-mailer-and-why-thats-a-good-thing-ddo</guid>
      <description>&lt;p&gt;If you went looking for &lt;code&gt;@forinda/kickjs-mailer&lt;/code&gt; on npm and found a deprecation banner instead of a v5 release, here's the punchline: the package isn't gone. It's just... yours. The framework still has everything you need to wire a mailer into the DI container — &lt;code&gt;defineAdapter&lt;/code&gt;, &lt;code&gt;createToken&lt;/code&gt;, lifecycle hooks — and the email surface turns out to be small enough that owning it in your own repo is the better trade. This post walks through what that pattern looks like, why it matters, and how to lay out the four pieces every BYO mailer collapses to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What got deprecated and why
&lt;/h2&gt;

&lt;p&gt;The README on the abandoned package is unusually direct. Quoting verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deprecated — going private in v4.1.2. Replaced by a BYO recipe using &lt;code&gt;defineAdapter&lt;/code&gt;/&lt;code&gt;definePlugin&lt;/code&gt; from &lt;code&gt;@forinda/kickjs&lt;/code&gt; directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence captures a philosophical shift in how the framework treats peripheral concerns. KickJS v5 still publishes opinionated packages for the things you don't want to reinvent — auth strategies, queue drivers, OpenTelemetry wiring, ORM adapters. But for surfaces that are essentially "an interface, a service, and a default implementation," shipping a package starts to cost more than it saves. You inherit a versioning pact ("does mailer 2.x work with kickjs 5.x?"), a peer-dep graph, and a public API that has to evolve in lockstep with three transports nobody on the maintainer's machine actually uses.&lt;/p&gt;

&lt;p&gt;The recipe approach inverts that. The framework guarantees the &lt;em&gt;primitives&lt;/em&gt; — &lt;code&gt;defineAdapter&lt;/code&gt;, the DI container, the request lifecycle. Recipes documenting how to assemble those primitives into common shapes live in the docs. Your project owns the assembled artifact. When the primitive's signature is stable across the major, your recipe-based mailer is stable across the major. No transitive churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe, not package
&lt;/h2&gt;

&lt;p&gt;A &lt;em&gt;package&lt;/em&gt; ships a finished thing — code, types, a published version, a public API contract. A &lt;em&gt;recipe&lt;/em&gt; ships a shape: "here's how to compose four primitives into a mailer; copy it, adjust it, own it." The framework promises that the primitives are stable. The recipe lives in your repo and serves only your app, so it's sized to your app — only the providers you actually use, only the options you actually configure. Fewer lines of code under your control instead of more lines of code under someone else's release schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four pieces
&lt;/h2&gt;

&lt;p&gt;Every BYO mailer collapses to the same four pieces. They don't change much between projects, which is exactly why the framework stopped trying to ship them as a unit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Types&lt;/strong&gt; — &lt;code&gt;MailMessage&lt;/code&gt;, &lt;code&gt;MailResult&lt;/code&gt;, &lt;code&gt;MailRecipient&lt;/code&gt;. The shape of what callers send and what they get back. Plain interfaces, no decorators, no DI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider interface&lt;/strong&gt; — &lt;code&gt;MailProvider&lt;/code&gt; with one method: &lt;code&gt;send(message): Promise&amp;lt;MailResult&amp;gt;&lt;/code&gt;. This is the seam tests stub, the seam ops swaps per environment, and the only contract a transport implementation has to satisfy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service&lt;/strong&gt; — &lt;code&gt;MailerService&lt;/code&gt;. The class injected into use-cases. It wraps a provider and adds the small amount of cross-cutting logic every call site would otherwise repeat — typically a default &lt;code&gt;from&lt;/code&gt; address, sometimes a logger, sometimes a tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter factory&lt;/strong&gt; — the &lt;code&gt;defineAdapter&lt;/code&gt; call. Produces a typed factory that registers the service against an injection token in the DI container during &lt;code&gt;beforeStart&lt;/code&gt;. Composition lives at the edge: pass it whichever provider this environment needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each piece is one screen of code. None depends on the framework beyond &lt;code&gt;defineAdapter&lt;/code&gt; and &lt;code&gt;createToken&lt;/code&gt;. That's the recipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the types look like
&lt;/h2&gt;

&lt;p&gt;The types are deliberately boring — interfaces, not classes, no inheritance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;MailRecipient&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailRecipient&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;MailRecipient&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MailResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAILER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailerService&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailerService&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful trick when migrating off a deprecated package: mirror the old shape on purpose. If your callers already say &lt;code&gt;mailer.send({ to, subject, html })&lt;/code&gt;, keep that exact signature. BYO doesn't have to mean "redesign your email API" — it usually means "lift the same shape, drop the dep." The injection token plays the same role: it's the contract between callers and the adapter, so use-cases reference &lt;code&gt;MAILER&lt;/code&gt; and never know which provider is actually wired underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The service stays small on purpose
&lt;/h2&gt;

&lt;p&gt;The service itself is small, and that's a feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailerService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;defaultFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailRecipient&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultFrom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; in there: no template engine, no retry policy, no rate limiting, no queue handoff. Those are real concerns for some apps — they're just not concerns for the &lt;em&gt;mailer service itself&lt;/em&gt;. Templating belongs in a render layer above; retries belong in a queue layer below. Keeping the service to "stamp &lt;code&gt;from&lt;/code&gt;, delegate" means it's still recognisably the same class six months from now when those layers do show up.&lt;/p&gt;

&lt;p&gt;If you find yourself reaching for an additional concern, ask whether it belongs &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;send&lt;/code&gt; or &lt;em&gt;around&lt;/em&gt; it. "Inside" is almost always the wrong answer. A 300-line mailer with hand-rolled exponential backoff is a code smell; pushing the message onto a queue and letting the queue owner handle retries is the shape that scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  A console provider for dev
&lt;/h2&gt;

&lt;p&gt;The most useful default provider in early development is one that logs to the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConsoleMailProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;console&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`console-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`mail[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; :: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero dependencies. Zero network calls. You can run the whole bootstrap, exercise password-reset, signup confirmation, anything that emits mail, and watch it scroll past in your dev terminal. When you're ready to send for real, you swap one constructor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The adapter factory
&lt;/h2&gt;

&lt;p&gt;The piece that ties it to the DI container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailerAdapterFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailerAdapterConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailerAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;MAILER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailerService&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;defaultFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultFrom&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createMailerAdapter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mailerAdapterFactory&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConsoleMailProvider&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;defaultFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noreply@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mailerAdapterFactory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two exports, two purposes. &lt;code&gt;createMailerAdapter()&lt;/code&gt; is the production ergonomics — call it from your bootstrap, get the dev-defaulted mailer registered. &lt;code&gt;mailerAdapterFactory&lt;/code&gt; is the &lt;em&gt;raw&lt;/em&gt; factory exposed for tests and per-environment composition: pass any &lt;code&gt;MailProvider&lt;/code&gt;, get an adapter that registers a service wrapping it. That's the entire surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test seam
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;MailProvider&lt;/code&gt; is a one-method interface, the test double is genuinely trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CapturingMailProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;capturing-test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`test-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each integration test instantiates a &lt;code&gt;CapturingMailProvider&lt;/code&gt;, threads it into the test container's mailer registration via the raw factory, and asserts on &lt;code&gt;subject&lt;/code&gt; / &lt;code&gt;html&lt;/code&gt; / &lt;code&gt;to&lt;/code&gt; after exercising the endpoint. If a flow embeds an OTP or a magic link in the email body, the test scrapes it back out of the captured payload — a thin helper on the test provider keeps that readable.&lt;/p&gt;

&lt;p&gt;Compare what stubbing the old package would have looked like: mock the export, stub &lt;code&gt;send&lt;/code&gt;, hope the mock signature matches whatever version of the package you happened to pull. Owning the interface means the stub is just a class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev to prod is one constructor swap
&lt;/h2&gt;

&lt;p&gt;This is where the recipe actually saves money. Today, dev runs on &lt;code&gt;ConsoleMailProvider&lt;/code&gt;. When transactional mail lands, swapping to a real transport is one constructor swap inside &lt;code&gt;createMailerAdapter()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConsoleMailProvider&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResendMailProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESEND_KEY&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="c1"&gt;// or&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SmtpMailProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service doesn't change. The injection token doesn't change. Every &lt;code&gt;mailer.send({...})&lt;/code&gt; call site doesn't change. There's no peer-dep version chase, no "does the new mailer package work with kickjs 5.7?", no transitive &lt;code&gt;nodemailer&lt;/code&gt; upgrade landing in your lockfile because the package author bumped a dep. You wrote ~30 lines of &lt;code&gt;ResendMailProvider&lt;/code&gt;, you own those 30 lines, and that's the whole change.&lt;/p&gt;

&lt;p&gt;The same composition pattern means staging on SMTP, prod on a transactional API, e2e on the capturing provider — same adapter factory, three different &lt;code&gt;provider:&lt;/code&gt; values. No package matrix to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should still ship a package
&lt;/h2&gt;

&lt;p&gt;Recipes win for surfaces that are basically "an interface plus a default." They lose when the default implementation has significant shared logic (token rotation, signature verification, multi-region routing) every caller would otherwise duplicate, when the contract has to evolve in coordination with an external spec, or when the thing genuinely benefits from being battle-tested across many consumers. Mailers fail all three. Once you notice the pattern, you start spotting it elsewhere: feature flags, audit logs, idempotency keys. Recipe-shaped surfaces all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is better
&lt;/h2&gt;

&lt;p&gt;Three concrete wins from doing it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller dep tree.&lt;/strong&gt; No mailer package means no &lt;code&gt;nodemailer&lt;/code&gt; (or whatever the package would have pulled in) until you actually want it. A console provider has zero deps. CI installs are faster, supply-chain surface is smaller, and you don't get CVE pings for transports you aren't using.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No upstream churn.&lt;/strong&gt; When kickjs ships v5.8, your mailer doesn't need a coordinated release. &lt;code&gt;defineAdapter&lt;/code&gt; is the API contract, and that's part of the framework core. As long as the core's stable, your recipe is stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trivial per-environment swaps.&lt;/strong&gt; Same adapter factory, different &lt;code&gt;provider:&lt;/code&gt; values per env. No package matrix, no dual-publishing dance, no "is the test provider on the same major as prod?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the trade KickJS v5 is pushing you toward across the small-surface concerns: own the recipe, lean on the primitives. The mailer is just the most readable example because everyone has written one.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/mailer.html" rel="noopener noreferrer"&gt;KickJS mailer recipe (v5 guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>mailer</category>
    </item>
    <item>
      <title>Adapters vs Plugins in KickJS v5 — Choosing the Right Primitive</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:32:50 +0000</pubDate>
      <link>https://dev.to/forinda/adapters-vs-plugins-in-kickjs-v5-choosing-the-right-primitive-1pp2</link>
      <guid>https://dev.to/forinda/adapters-vs-plugins-in-kickjs-v5-choosing-the-right-primitive-1pp2</guid>
      <description>&lt;p&gt;You read the adapters article. You understood &lt;code&gt;defineAdapter&lt;/code&gt;. You went to wire up your next concern, opened the v5 docs, and got hit with a second word: &lt;code&gt;definePlugin&lt;/code&gt;. The shapes look almost identical — both take a &lt;code&gt;name&lt;/code&gt;, both can return &lt;code&gt;middleware()&lt;/code&gt;, both can register DI tokens, both can contribute context decorators. Hovering the types in your editor only deepens the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I just need to register a service. &lt;code&gt;defineAdapter&lt;/code&gt; or &lt;code&gt;definePlugin&lt;/code&gt;?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is the answer. Short version: the surfaces overlap because they were designed to. The difference is &lt;strong&gt;identity, lifecycle, and distribution&lt;/strong&gt; — not capability. Once you internalize that, the choice becomes mechanical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an adapter actually is
&lt;/h2&gt;

&lt;p&gt;An adapter is a piece of &lt;strong&gt;app-level infrastructure&lt;/strong&gt; that owns long-lived resources and plugs into the bootstrap lifecycle. The first article covered the lifecycle in depth; the short version is that an &lt;code&gt;AppAdapter&lt;/code&gt; is a typed object the framework calls in a fixed order: &lt;code&gt;beforeMount&lt;/code&gt; → &lt;code&gt;beforeStart&lt;/code&gt; → &lt;code&gt;middleware()&lt;/code&gt; collected → &lt;code&gt;onRouteMount&lt;/code&gt; per controller → &lt;code&gt;afterStart&lt;/code&gt; → (requests served) → &lt;code&gt;shutdown&lt;/code&gt; on &lt;code&gt;SIGTERM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The mental model is "the bootstrap machinery composes a fleet of these, in order, exactly once per process." Adapters are &lt;strong&gt;single-instance by identity&lt;/strong&gt;. A typical app has one DB-pool adapter, one mailer adapter, one observability adapter — they own resources nobody else should own (connection pools, tracer SDKs, mail transports). Two of any of them would mean two pools, which is almost always a bug.&lt;/p&gt;

&lt;p&gt;Adapters are also &lt;strong&gt;declared at the application layer&lt;/strong&gt;. They live next to &lt;code&gt;bootstrap()&lt;/code&gt; because they know things only the application knows: which env vars matter, which secrets provider to use, which cluster URL to dial. They aren't packaged for redistribution because they aren't generic — they are &lt;em&gt;this app's&lt;/em&gt; infrastructure.&lt;/p&gt;

&lt;p&gt;The shape, in miniature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DbAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* drain pool */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every lifecycle hook is on the table. Resources you create in &lt;code&gt;beforeStart&lt;/code&gt; you tear down in &lt;code&gt;shutdown&lt;/code&gt;. That symmetry is the whole point of the primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a plugin actually is
&lt;/h2&gt;

&lt;p&gt;A plugin is a &lt;strong&gt;reusable building block&lt;/strong&gt; packaged for redistribution. It can ship modules, adapters, middleware, contributors, and DI bindings, and an app opts in by passing it to &lt;code&gt;bootstrap({ plugins: [...] })&lt;/code&gt;. The shape is intentionally similar to an adapter's, but the framing is different: a plugin is a &lt;strong&gt;bundle that travels&lt;/strong&gt; — across apps, across teams, often as an npm package.&lt;/p&gt;

&lt;p&gt;In its smallest form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RateLimitPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;definePlugin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RateLimitPlugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LIMITS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;rateLimitMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plugin can also expose an &lt;code&gt;adapters()&lt;/code&gt; slot — a common pattern for "give me the whole subsystem in one import." Three properties matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Opt-in registration.&lt;/strong&gt; Adapters live in the app's adapter array; plugins live in the app's plugins array. The plugins array is the shape that travels in &lt;code&gt;README&lt;/code&gt; snippets — &lt;code&gt;bootstrap({ plugins: [RateLimitPlugin({ rps: 50 })] })&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple per app are normal.&lt;/strong&gt; An app can pull in observability, CORS, MFA, rate-limiting, and feature-flag plugins side by side. They run before the application bootstraps proper, in &lt;code&gt;dependsOn&lt;/code&gt;-aware order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designed for redistribution.&lt;/strong&gt; &lt;code&gt;definePlugin&lt;/code&gt; carries a &lt;code&gt;version&lt;/code&gt;, a &lt;code&gt;requires.kickjs&lt;/code&gt; peer-version, and a &lt;code&gt;defaults&lt;/code&gt; config slot — all the surface a published npm package needs to declare compatibility and let consumers override config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A plugin doesn't &lt;em&gt;have&lt;/em&gt; to be published — in-repo plugins are fine — but the design choices make sense only when you read them as "what do I need so a stranger can &lt;code&gt;npm install&lt;/code&gt; this and bootstrap it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;p&gt;Walk these in order. The first row that fits is your answer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is it the application's own infrastructure (DB pool, secrets, observability, mailer transport, tenant resolver)?&lt;/strong&gt; → adapter. It belongs next to &lt;code&gt;bootstrap()&lt;/code&gt; with the rest of the app's wiring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it own a long-lived resource that needs &lt;code&gt;beforeStart&lt;/code&gt; to construct and &lt;code&gt;shutdown&lt;/code&gt; to drain?&lt;/strong&gt; → adapter. The lifecycle was built for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Should there be exactly one in the process, ever?&lt;/strong&gt; → adapter. Two infrastructure adapters in the same app means two of whatever the adapter owns — usually a bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it generic enough that another team or app could plausibly use it unchanged?&lt;/strong&gt; → plugin. Even if you keep it in-repo today, model it as a plugin so the move-out cost is zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it bundle a module + DI registration + (maybe) an adapter into one import?&lt;/strong&gt; → plugin. The &lt;code&gt;adapters()&lt;/code&gt; slot exists for exactly this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Will it ship as an npm package, with its own &lt;code&gt;version&lt;/code&gt; and a &lt;code&gt;requires.kickjs&lt;/code&gt; range?&lt;/strong&gt; → plugin. The &lt;code&gt;definePlugin&lt;/code&gt; shape is an npm-package surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Will multiple consumers configure it differently (different keys, transports, rules)?&lt;/strong&gt; → plugin. The &lt;code&gt;defaults&lt;/code&gt; + caller-overrides story is built for opt-in callers passing config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;adapters are nouns this app owns; plugins are nouns this app installs.&lt;/strong&gt; If you're hesitating, ask "could I &lt;code&gt;npm publish&lt;/code&gt; this tomorrow and have it make sense to someone who has never seen our codebase?" If yes, plugin. If the answer is "no, this only makes sense given our env, our DB, our secrets layout," adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough — when a mailer is an adapter
&lt;/h2&gt;

&lt;p&gt;Picture an app that needs to send transactional email. It picks a provider (SES, Postmark, console-logging in dev), configures a default sender, and exposes a &lt;code&gt;MailerService&lt;/code&gt; to the rest of the codebase. You could package this as a plugin. Most of the time you shouldn't — adapter form is the better fit.&lt;/p&gt;

&lt;p&gt;A sketch of the adapter form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MailerAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailerAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAILER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MailerService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make this an adapter, not a plugin:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-instance by identity.&lt;/strong&gt; The app has exactly one &lt;code&gt;MailerService&lt;/code&gt;. The &lt;code&gt;MAILER&lt;/code&gt; token is registered once. Two mailer adapters would clash on the token and double-bill the SMTP transport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App-level composition.&lt;/strong&gt; The default sender address is &lt;em&gt;this app's&lt;/em&gt; identity. The provider choice (console in dev/test, real transport in prod) is &lt;em&gt;this app's&lt;/em&gt; environment story. None of that travels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No redistribution case.&lt;/strong&gt; "A &lt;code&gt;MailProvider&lt;/code&gt; plus a thin service that picks one based on config" is roughly ten lines per consumer. Anybody else writing a v5 app would write their own adapter the same way. The plugin shape (versioning, peer range, defaults-merging, npm packaging) would be ceremony with no payoff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plugin form would become appropriate the moment you wanted to ship a sender, retry policy, templating engine, and provider matrix as a packaged unit — at that point the bundle has its own version, its own peer-version range, and its own consumers. Until then, a small adapter wins on every axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough — when a feature wants to be a plugin
&lt;/h2&gt;

&lt;p&gt;Now picture a second-factor auth feature: issue a TOTP secret, verify a one-time code, recover via backup codes. It's a different shape entirely, and adapter form would fight you the whole way.&lt;/p&gt;

&lt;p&gt;A sketch of the plugin form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TotpPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;definePlugin&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encryptionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TotpPlugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CIPHER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;makeCipher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nf"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TotpModule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is this a plugin?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a bundle, not a single piece of infrastructure.&lt;/strong&gt; It ships a module (controllers + routes), DI tokens (a secret cipher, repos), and a config surface (the encryption key). The plugin shape was designed precisely for "give me the whole subsystem in one import." Adapter form would force splitting these into separate registrations on the application side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The encryption key is consumer-supplied configuration.&lt;/strong&gt; A plugin's &lt;code&gt;defaults&lt;/code&gt; plus caller-overrides ergonomics fit this naturally. Different apps pass different keys, which is exactly the "config is opt-in" story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's pre-built for redistribution.&lt;/strong&gt; Maybe today it has one consumer. The cost of plugin form (one more workspace package) is small; the cost of &lt;em&gt;not&lt;/em&gt; doing it and later having to repackage during a real rollout is much larger. Plugin form is cheap insurance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It composes with &lt;code&gt;dependsOn&lt;/code&gt;.&lt;/strong&gt; Plugins participate in topological sort. If a future MFA-enforcement plugin needs to mount after TOTP, it just declares &lt;code&gt;dependsOn: ['TotpPlugin']&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the tell. Anything that might be one of &lt;em&gt;several&lt;/em&gt; peer building blocks — auth methods, instrumentation backends, rate-limit strategies — wants to be a plugin so the next sibling can compose with it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both can register middleware and contributors
&lt;/h2&gt;

&lt;p&gt;The two registration surfaces overlap on purpose. Adapters can &lt;code&gt;middleware()&lt;/code&gt;; plugins can &lt;code&gt;middleware()&lt;/code&gt;. Adapters can &lt;code&gt;contributors()&lt;/code&gt;; plugins can &lt;code&gt;contributors()&lt;/code&gt;. Adapters can register DI bindings (in &lt;code&gt;beforeStart&lt;/code&gt;); plugins can register DI bindings (in &lt;code&gt;register&lt;/code&gt;). The framework even merges contributors at the same &lt;code&gt;'adapter'&lt;/code&gt; precedence level for both — a plugin is a cross-cutting bundle, narrower than the global default but broader than a per-module hook, and that places it on the same precedence band as adapter-supplied contributors.&lt;/p&gt;

&lt;p&gt;This equivalence is deliberate. The capabilities are kept symmetric so you don't pick the wrong primitive and discover three weeks later it can't do something. &lt;strong&gt;The decision is identity, lifecycle ownership, and distribution — not features.&lt;/strong&gt; Pick adapter for app-level infrastructure with a single owner. Pick plugin for portable bundles meant to be configured and reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful litmus test
&lt;/h2&gt;

&lt;p&gt;When you're stuck, write the README snippet you'd want a future consumer to paste. If it reads naturally as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;MyThing&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and the &lt;code&gt;key: '...'&lt;/code&gt; makes sense as something &lt;em&gt;callers&lt;/em&gt; override — you're holding a plugin. If instead the only sensible call site is your own app's adapter file, with config pulled from your own &lt;code&gt;env&lt;/code&gt;, and there's no plausible second consumer — you're holding an adapter. Don't fight the shape. The two primitives exist precisely so each kind of concern has a home that fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/plugins.html" rel="noopener noreferrer"&gt;KickJS docs — plugins guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/adapters.html" rel="noopener noreferrer"&gt;KickJS docs — adapters guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The KickJS Request Lifecycle — Middleware, Contributors, and Typed Context</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:31:35 +0000</pubDate>
      <link>https://dev.to/forinda/the-kickjs-request-lifecycle-middleware-contributors-and-typed-context-fo</link>
      <guid>https://dev.to/forinda/the-kickjs-request-lifecycle-middleware-contributors-and-typed-context-fo</guid>
      <description>&lt;p&gt;There is a tempting first read of KickJS where &lt;code&gt;ctx&lt;/code&gt; looks like a friendlier wrapper around Express's &lt;code&gt;req&lt;/code&gt;/&lt;code&gt;res&lt;/code&gt;. It is not. Treating &lt;code&gt;ctx&lt;/code&gt; as "&lt;code&gt;req&lt;/code&gt; in disguise" is the single fastest way to write code that fights the framework — you reach for &lt;code&gt;req.user&lt;/code&gt;, you read headers off &lt;code&gt;req.headers&lt;/code&gt;, you sprinkle &lt;code&gt;requireX(ctx)&lt;/code&gt; helpers around handlers, and the type system never quite catches up. Once you understand that &lt;code&gt;ctx&lt;/code&gt; is a typed, contributor-populated object that lives on top of a per-request store — and that the lifecycle is staged across phases the framework actually controls — the whole stack starts pulling in the same direction. This article walks the KickJS request lifecycle end to end as a conceptual model you can apply to any KickJS app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phases, in firing order
&lt;/h2&gt;

&lt;p&gt;A request to a KickJS app is not a flat chain. It moves through three distinct stages, in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request
    │
    ▼
┌─────────────────────── Adapter middleware ───────────────────────┐
│  beforeGlobal   →  before kickjs's framework middleware          │
│  afterGlobal    →  after framework middleware, BEFORE routes     │
│  beforeRoutes   →  per-router, before the matched handler        │
│  afterRoutes    →  per-router, after the handler resolves        │
└──────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────── Per-route contributors ───────────────────┐
│  Pipeline built from `bootstrap({ contributors: [...] })`        │
│  Each contributor's `resolve(ctx)` runs once, result cached      │
│  in the per-request store under its `key`                        │
└──────────────────────────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────── Controller handler ───────────────────────┐
│  async handler(ctx: Ctx&amp;lt;KickRoutes.Foo['op']&amp;gt;) {                 │
│    const session = ctx.get('session')   // typed, never unknown  │
│  }                                                               │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adapter middleware is where infrastructure lives — body parsing, auth strategies, request shaping. Contributors are where derived, per-request values get computed, typed, and cached. The handler is where business logic actually runs. The line between these stages is rigid on purpose: middleware mutates &lt;code&gt;req&lt;/code&gt;, contributors read &lt;code&gt;ctx&lt;/code&gt;, handlers consume both via &lt;code&gt;ctx.get()&lt;/code&gt; and the typed &lt;code&gt;Ctx&amp;lt;T&amp;gt;&lt;/code&gt; shape from &lt;code&gt;KickRoutes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The four middleware phases — &lt;code&gt;beforeGlobal&lt;/code&gt;, &lt;code&gt;afterGlobal&lt;/code&gt;, &lt;code&gt;beforeRoutes&lt;/code&gt;, &lt;code&gt;afterRoutes&lt;/code&gt; — are not aesthetic. They give you a deterministic place to plug in things like "I need to run before kickjs registers its own framework middleware" (&lt;code&gt;beforeGlobal&lt;/code&gt;) versus "I need every request to have a session attached, but I do not want to be inside any specific router yet" (&lt;code&gt;afterGlobal&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapter middleware
&lt;/h2&gt;

&lt;p&gt;A typical adapter mounts handlers at the phases it cares about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AdapterMiddleware&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;afterGlobal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookupSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. First: the middleware does not touch &lt;code&gt;ctx&lt;/code&gt;. It mutates the underlying Express &lt;code&gt;Request&lt;/code&gt; only. Second: it is registered as &lt;code&gt;phase: 'afterGlobal'&lt;/code&gt; precisely because it must run after KickJS's framework middleware (so that body parsing, request id, etc. are in place) but before any router or contributor sees the request. If you put it at &lt;code&gt;beforeRoutes&lt;/code&gt; you would have to mount it on every router. If you put it at &lt;code&gt;beforeGlobal&lt;/code&gt; you would race the framework's own initialization. &lt;code&gt;afterGlobal&lt;/code&gt; is the right joint for cross-cutting infrastructure that every route depends on.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;beforeRoutes&lt;/code&gt;/&lt;code&gt;afterRoutes&lt;/code&gt; for per-router concerns (a logging shim around one feature module, a guard that only one router exposes). Use &lt;code&gt;beforeGlobal&lt;/code&gt; sparingly — it sits in front of the framework's own request id, error handling, and body parsing, so most of the time it is not what you want.&lt;/p&gt;

&lt;p&gt;The pattern to internalize is: &lt;strong&gt;adapter middleware stamps things onto &lt;code&gt;req&lt;/code&gt;. Contributors lift those things into the typed &lt;code&gt;ctx&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributors: typed &lt;code&gt;ctx.get()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;A contributor is just an object built by &lt;code&gt;defineHttpContextDecorator&lt;/code&gt;. The minimal shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineHttpContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four properties matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt; — the string that handlers will use in &lt;code&gt;ctx.get('session')&lt;/code&gt;. It is also the property in the &lt;code&gt;ContextMeta&lt;/code&gt; interface that gives the read its type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resolve(ctx)&lt;/code&gt;&lt;/strong&gt; — the function that produces the value. Runs once per request, lazily, the first time something asks for it. The return value is cached in the per-request store under &lt;code&gt;key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;optional&lt;/code&gt;&lt;/strong&gt; — when &lt;code&gt;true&lt;/code&gt;, a throw inside &lt;code&gt;resolve&lt;/code&gt; is swallowed silently and the key is simply not set; &lt;code&gt;ctx.get('key')&lt;/code&gt; returns &lt;code&gt;undefined&lt;/code&gt;. When false (the default), a throw aborts the request with whatever &lt;code&gt;HttpException&lt;/code&gt; you raised. Use &lt;code&gt;optional: true&lt;/code&gt; for values that legitimately may not exist (anonymous endpoints, public webhooks) so the request does not 500 just because the contributor cannot build its value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dependsOn&lt;/code&gt;&lt;/strong&gt; — an array of other contributor keys that must resolve first. KickJS's &lt;code&gt;buildPipeline&lt;/code&gt; topologically sorts contributors using these dependencies. If &lt;code&gt;LoadProfile&lt;/code&gt; declared &lt;code&gt;dependsOn: ['session']&lt;/code&gt;, the pipeline would guarantee &lt;code&gt;LoadSession&lt;/code&gt; ran first. If a contributor declared a dependency on a key that no contributor exposes, &lt;code&gt;buildPipeline&lt;/code&gt; would reject the registration at boot — the framework refuses to silently ignore a missing dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you call &lt;code&gt;bootstrap({ contributors: [LoadSession.registration, ...] })&lt;/code&gt;, those contributors run on every route automatically. Handlers consume the result with &lt;code&gt;ctx.get('session')&lt;/code&gt; — typed — instead of reading &lt;code&gt;ctx.req.session&lt;/code&gt; and reinventing the null check at every callsite.&lt;/p&gt;

&lt;p&gt;The contract is simple: if you read a value via &lt;code&gt;ctx.get&lt;/code&gt;, the framework either gave you that value or threw an &lt;code&gt;HttpException&lt;/code&gt; with a message you can grep for. There is no "sometimes typed, sometimes undefined" middle ground muddying handler code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Augmenting &lt;code&gt;ContextMeta&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ctx.get('session')&lt;/code&gt; is typed, but only because the app told the framework what &lt;code&gt;session&lt;/code&gt; actually is. The augmentation lives in a &lt;code&gt;.d.ts&lt;/code&gt; file your app owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Session&lt;/span&gt;
    &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;declare module&lt;/code&gt; block is the one TypeScript trick that makes the entire &lt;code&gt;ctx.get()&lt;/code&gt; story worth using. KickJS's &lt;code&gt;ContextMeta&lt;/code&gt; interface is defined empty in the framework. When your app augments it, every call to &lt;code&gt;ctx.get&amp;lt;K extends keyof ContextMeta&amp;gt;(k: K): ContextMeta[K] | undefined&lt;/code&gt; (or the non-&lt;code&gt;undefined&lt;/code&gt; overload when the contributor is non-optional) returns the narrowed type instead of &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trailing &lt;code&gt;export {}&lt;/code&gt; is required to keep the file a module rather than a script — without it, &lt;code&gt;declare module&lt;/code&gt; does not augment correctly. If you skip the &lt;code&gt;declare module&lt;/code&gt; block entirely, every &lt;code&gt;ctx.get()&lt;/code&gt; returns &lt;code&gt;unknown&lt;/code&gt; and your handlers fight the type system instead of leaning on it. This file is small but every line is load-bearing.&lt;/p&gt;

&lt;p&gt;A useful discipline: keep one &lt;code&gt;context-meta.d.ts&lt;/code&gt; per app. Domain packages that ship contributors should export the type they augment, and the app composing them does the actual &lt;code&gt;declare module&lt;/code&gt; once. That keeps augmentation centralized and easy to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;dependsOn&lt;/code&gt; trap
&lt;/h2&gt;

&lt;p&gt;Here is the canonical bug. Suppose a contributor needs the user that an auth strategy populates onto &lt;code&gt;req.user&lt;/code&gt;, and you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineHttpContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// looks reasonable. it isn't.&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;buildProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent is correct: the profile needs the user. The problem is that &lt;code&gt;user&lt;/code&gt; is not a contributor. &lt;code&gt;req.user&lt;/code&gt; is populated by the auth strategy's adapter middleware, which runs in the &lt;code&gt;afterGlobal&lt;/code&gt;/&lt;code&gt;beforeRoutes&lt;/code&gt; phase before any contributor pipeline executes. There is no &lt;code&gt;LoadUser.registration&lt;/code&gt; in the &lt;code&gt;bootstrap({ contributors })&lt;/code&gt; array — there cannot be, because the auth adapter owns that responsibility.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildPipeline&lt;/code&gt; checks every &lt;code&gt;dependsOn&lt;/code&gt; key against the registered contributor set. When it sees &lt;code&gt;dependsOn: ['user']&lt;/code&gt; and finds no contributor with &lt;code&gt;key: 'user'&lt;/code&gt;, it raises a &lt;code&gt;MissingContributorError&lt;/code&gt; at boot. The fix is to drop the false dependency and trust the lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineHttpContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// no dependsOn — auth middleware ran in afterGlobal,&lt;/span&gt;
  &lt;span class="c1"&gt;// so ctx.req.user is already populated by the time resolve runs&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;buildProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mental model to keep is: &lt;strong&gt;&lt;code&gt;dependsOn&lt;/code&gt; is for ordering between contributors, not for declaring "I need something on &lt;code&gt;ctx&lt;/code&gt;."&lt;/strong&gt; If the value comes from middleware, you read it inside &lt;code&gt;resolve&lt;/code&gt; and rely on phase ordering. If the value comes from another contributor, you declare the edge and let the framework topo-sort. Mixing the two produces boot-time errors that look mysterious until you remember which layer owns which keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why contributors &amp;gt; helpers
&lt;/h2&gt;

&lt;p&gt;Before contributors, the natural pattern is a &lt;code&gt;requireX(ctx)&lt;/code&gt; helper imported into every handler that needs &lt;code&gt;X&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requireSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// throws 401 if missing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That helper is fine in isolation. It is also duplicated logic, untyped at the framework level (the helper returns the right type only because the helper itself is typed — the framework does not know about it), and impossible to compose with auth or audit concerns without a second helper, then a third. The pattern makes the framework lifecycle invisible: the throw happens wherever the helper got called, so the same "no session" error surfaces in twelve different stack frames.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ctx.get('session')&lt;/code&gt; collapses all of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The error message lives in one place — inside &lt;code&gt;LoadSession.resolve&lt;/code&gt;. Change it once, the whole app updates.&lt;/li&gt;
&lt;li&gt;The retrieval is typed via &lt;code&gt;ContextMeta&lt;/code&gt; augmentation. No helper indirection.&lt;/li&gt;
&lt;li&gt;The framework decides when &lt;code&gt;resolve&lt;/code&gt; runs, caches the value, and integrates with &lt;code&gt;optional&lt;/code&gt; for routes that legitimately do not need it.&lt;/li&gt;
&lt;li&gt;Adding a new derived value is a registration, not a new helper file with its own import surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A handler now just looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ctx&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;KickRoutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WidgetsController&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createWidget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toWidgetDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ctx.body&lt;/code&gt; is typed from the route's Zod schema. &lt;code&gt;ctx.get('profile')&lt;/code&gt; is typed from &lt;code&gt;ContextMeta&lt;/code&gt;. The &lt;code&gt;!&lt;/code&gt; is the explicit acknowledgement that this route is authenticated, so the optional contributor will have resolved. There is no helper, no &lt;code&gt;requireSession&lt;/code&gt;, no per-handler null check. The framework handled it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Read the request as a pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Express receives the request. KickJS's framework middleware runs (&lt;code&gt;beforeGlobal&lt;/code&gt; adapter middleware first, then framework internals).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;afterGlobal&lt;/code&gt; middleware fires. Auth strategies, session resolvers, and other cross-cutting infrastructure stamp values onto &lt;code&gt;req&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The route is matched. &lt;code&gt;beforeRoutes&lt;/code&gt; middleware runs (per-router auth guards, role checks, etc.).&lt;/li&gt;
&lt;li&gt;The contributor pipeline executes. Each registered contributor's &lt;code&gt;resolve&lt;/code&gt; runs once, in topological order based on &lt;code&gt;dependsOn&lt;/code&gt;. Results are cached in the per-request store.&lt;/li&gt;
&lt;li&gt;The handler runs. &lt;code&gt;ctx.get(key)&lt;/code&gt; returns typed values; &lt;code&gt;ctx.body&lt;/code&gt;/&lt;code&gt;ctx.params&lt;/code&gt;/&lt;code&gt;ctx.query&lt;/code&gt; are typed from &lt;code&gt;KickRoutes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;afterRoutes&lt;/code&gt; middleware fires for cleanup (logging, response shaping). The response goes back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each layer has one job. Middleware mutates the request. Contributors compute and cache derived values. Handlers consume typed &lt;code&gt;ctx&lt;/code&gt;. The framework polices the boundaries — &lt;code&gt;dependsOn&lt;/code&gt; enforces contributor ordering, &lt;code&gt;ContextMeta&lt;/code&gt; enforces typed reads, &lt;code&gt;MissingContributorError&lt;/code&gt; blocks boot when the wiring is wrong.&lt;/p&gt;

&lt;p&gt;Once you internalize this, the helper-per-concept style starts to feel like writing your own framework on the side. Contributors are not magic; they are just a place for derived per-request state to live with type safety, lifecycle integration, and a single throw site. That is what makes &lt;code&gt;ctx&lt;/code&gt; worth more than &lt;code&gt;req&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/context-decorators.html" rel="noopener noreferrer"&gt;KickJS docs — context decorators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>middleware</category>
    </item>
    <item>
      <title>Authentication and Authorization in KickJS — Strategies, Roles, and Type-Safe Decorators</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:29:48 +0000</pubDate>
      <link>https://dev.to/forinda/authentication-and-authorization-in-kickjs-strategies-roles-and-type-safe-decorators-30mf</link>
      <guid>https://dev.to/forinda/authentication-and-authorization-in-kickjs-strategies-roles-and-type-safe-decorators-30mf</guid>
      <description>&lt;p&gt;For most of last year, the cheapest way to ship a security regression in a TypeScript API was a single typo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owener&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// shipped. nobody noticed. forever-403.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@Roles&lt;/code&gt; accepted any string. Tests still passed because they used the spelled-correctly literal. The route silently rejected every request. KickJS v5 fixes this: &lt;code&gt;@Roles&lt;/code&gt; is now narrowed by your project's own role union via a one-file &lt;code&gt;declare module&lt;/code&gt; augmentation, so misspellings become a compile error before they leave the editor. This post walks the full auth surface in KickJS — &lt;code&gt;AuthAdapter&lt;/code&gt;, the &lt;code&gt;AuthStrategy&lt;/code&gt; interface, &lt;code&gt;@Authenticated&lt;/code&gt;, &lt;code&gt;@Roles&lt;/code&gt;, &lt;code&gt;@Public&lt;/code&gt; — at a conceptual level, with small generic snippets you can map onto any KickJS app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of auth in KickJS
&lt;/h2&gt;

&lt;p&gt;KickJS keeps auth deliberately small. There are three concepts to learn, and they compose cleanly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AuthAdapter&lt;/code&gt;&lt;/strong&gt; — an &lt;code&gt;AppAdapter&lt;/code&gt; (the same plug used for observability, swagger, db) constructed via &lt;code&gt;createAuthAdapter()&lt;/code&gt;. You hand it a &lt;code&gt;defaultPolicy&lt;/code&gt; (&lt;code&gt;'protected'&lt;/code&gt; or &lt;code&gt;'public'&lt;/code&gt;) and an array of strategies. It registers a per-request middleware that runs strategies in order, attaches the resolved user to &lt;code&gt;req.user&lt;/code&gt;, and returns 401 on failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AuthStrategy&lt;/code&gt;&lt;/strong&gt; — an interface: &lt;code&gt;name: string&lt;/code&gt; plus &lt;code&gt;validate(req): Promise&amp;lt;AuthUser | null&amp;gt;&lt;/code&gt;. Built-ins live in &lt;code&gt;@forinda/kickjs-auth&lt;/code&gt; (&lt;code&gt;JwtStrategy&lt;/code&gt;, &lt;code&gt;ApiKeyStrategy&lt;/code&gt;); custom strategies are just classes implementing the interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decorators&lt;/strong&gt; — &lt;code&gt;@Authenticated('strategy-name')&lt;/code&gt; opts a controller or route into a specific strategy; &lt;code&gt;@Roles(...)&lt;/code&gt; narrows the user's role; &lt;code&gt;@Public()&lt;/code&gt; marks a route as anonymous. They compose at any level — controller-class, method, or both.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The big idea: &lt;strong&gt;strategy choice and role check are separate axes&lt;/strong&gt;. &lt;code&gt;@Authenticated&lt;/code&gt; says &lt;em&gt;who is allowed to identify you&lt;/em&gt;; &lt;code&gt;@Roles&lt;/code&gt; says &lt;em&gt;what they need to be&lt;/em&gt;. Some routes only need the first (a synthetic system user has no roles); some need both (regular CRUD needs JWT plus an authorized role).&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring AuthAdapter
&lt;/h2&gt;

&lt;p&gt;A typical adapter file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createAuthAdapter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AppAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AuthAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;defaultPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;protected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nc"&gt;JwtStrategy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ...covered next... */&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ServiceTokenStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two production-grade habits to copy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defaultPolicy: 'protected'&lt;/code&gt;.&lt;/strong&gt; Every route requires a valid user unless explicitly &lt;code&gt;@Public()&lt;/code&gt;. The opposite default (&lt;code&gt;'public'&lt;/code&gt;) is the path to forgetting &lt;code&gt;@Authenticated&lt;/code&gt; on a sensitive endpoint and shipping it. Default-deny costs you &lt;code&gt;@Public()&lt;/code&gt; on three routes (health, login, refresh) and protects everything else automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuse to construct against an unresolved secret.&lt;/strong&gt; If your env layer keeps secrets symbolic in committed config and a resolver swaps them at boot, the adapter must throw when it finds a placeholder value — otherwise it would silently sign or verify with the literal placeholder string. Steal the pattern: any factory that consumes a secret should refuse to build against an unresolved sentinel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The adapter is a factory, not a class. KickJS auth v4 dropped construct signatures on &lt;code&gt;AuthAdapter&lt;/code&gt; and &lt;code&gt;JwtStrategy&lt;/code&gt;; calling them with &lt;code&gt;new&lt;/code&gt; raises TS7009. If you're migrating from v3, that's the diff to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  JWT with claim mapping
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;JwtStrategy&lt;/code&gt; does the heavy lifting — Bearer parsing, signature verify, expiry. The interesting bit is &lt;code&gt;mapPayload&lt;/code&gt;, which turns a verified JWT payload into your app's user shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;JwtStrategy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;mapPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AppAuthUser&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_ISSUER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iss mismatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;claimed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth flagging:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issuer/audience enforcement inside &lt;code&gt;mapPayload&lt;/code&gt;.&lt;/strong&gt; Some &lt;code&gt;JwtStrategy&lt;/code&gt; versions don't forward &lt;code&gt;issuer&lt;/code&gt;/&lt;code&gt;audience&lt;/code&gt; to the underlying verifier. The workaround is small: do the comparison yourself and throw on mismatch. Throwing inside &lt;code&gt;mapPayload&lt;/code&gt; bubbles up as a strategy failure — the adapter returns 401, the handler never runs. If a future version adds first-class options, the migration is deleting a few lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role filtering as defense in depth.&lt;/strong&gt; Filter the JWT's &lt;code&gt;roles&lt;/code&gt; claim through your known role union — drop anything else silently. Real validation belongs at issue time (the login path checks against your membership store), but defense in depth is cheap here and stops a class of bugs where downstream code accidentally trusts an unknown role string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Returning a typed user.&lt;/strong&gt; The return type extends &lt;code&gt;AuthUser&lt;/code&gt; with whatever your app needs (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, a typed &lt;code&gt;roles&lt;/code&gt; array, maybe a &lt;code&gt;jti&lt;/code&gt; for revocation). Because of the augmentation we'll see next, downstream &lt;code&gt;@Roles(...)&lt;/code&gt; calls share that exact union.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second strategy for a second audience
&lt;/h2&gt;

&lt;p&gt;Sooner or later you have routes that aren't part of your normal user-token flow — service-to-service callers, a control plane, a CI bot, an internal admin endpoint. Stuffing them into the same JWT issuer means either issuing privileged tokens out of the user login path or carrying a flag in user tokens that breaks isolation. Don't do either. Add a second strategy.&lt;/p&gt;

&lt;p&gt;A minimal shared-token strategy looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceTokenStrategy&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;AuthStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppAuthUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERVICE_TOKEN&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provided&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;want&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes worth internalizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;timingSafeEqual&lt;/code&gt;.&lt;/strong&gt; Constant-time comparison stops the trivial timing attack on simple-token auth. If you're going to ship a shared bearer at all, do this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read &lt;code&gt;process.env&lt;/code&gt; directly when you want HMR and test-stubs to "just work".&lt;/strong&gt; Cached env snapshots are great for app config but get in the way for strategies that need to reflect runtime changes during tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthetic users are fine.&lt;/strong&gt; A service caller doesn't need a row in your users table. A constant &lt;code&gt;{ id: 'service', roles: [] }&lt;/code&gt; is enough — the strategy &lt;em&gt;is&lt;/em&gt; the gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now look at registration order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nc"&gt;JwtStrategy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ServiceTokenStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Order matters.&lt;/strong&gt; KickJS picks "the first strategy that returns a user." JWT runs first, so user routes resolve their JWT-derived user. The service strategy only matches when a route opts in via &lt;code&gt;@Authenticated('service-token')&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Authenticated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InternalOpsController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combination of (default-deny + ordered strategies + opt-in &lt;code&gt;@Authenticated('strategy-name')&lt;/code&gt;) is what keeps the two audiences cleanly separated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Type-safe @Roles via AuthUser augmentation
&lt;/h2&gt;

&lt;p&gt;Here's the piece that earns its keep. &lt;code&gt;@Roles(...)&lt;/code&gt; in KickJS v5 is generic over &lt;code&gt;AuthUser['roles'][number]&lt;/code&gt;. Inside &lt;code&gt;@forinda/kickjs-auth&lt;/code&gt; the relevant type is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AuthUser&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;roles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;MethodDecorator&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ClassDecorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default &lt;code&gt;AuthUser['roles']&lt;/code&gt; is &lt;code&gt;string[]&lt;/code&gt;, so &lt;code&gt;Role&lt;/code&gt; resolves to &lt;code&gt;string&lt;/code&gt; — which is why old &lt;code&gt;@Roles('typo')&lt;/code&gt; compiled. The fix is a one-file declaration merge in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/types/auth-augment.d.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/auth/roles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthUser&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define your role union in exactly one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/auth/roles.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the augmentation, &lt;code&gt;AuthUser['roles']&lt;/code&gt; is &lt;code&gt;AppRole[]&lt;/code&gt; everywhere — including inside the framework, including inside the &lt;code&gt;Roles&lt;/code&gt; decorator's generic. The package uses an &lt;code&gt;IsAny&lt;/code&gt;-pivoted conditional to pick this up cleanly: if user code augments &lt;code&gt;AuthUser&lt;/code&gt;, &lt;code&gt;Role&lt;/code&gt; resolves to your union; if not, it falls back to &lt;code&gt;string&lt;/code&gt; so unconfigured projects still compile.&lt;/p&gt;

&lt;p&gt;Concretely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Authenticated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostsController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Roles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@Roles('owner', 'admin')&lt;/code&gt; typechecks. &lt;code&gt;@Roles('owener', 'admin')&lt;/code&gt; doesn't — TS reports &lt;code&gt;Argument of type '"owener"' is not assignable to parameter of type 'AppRole'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the proof. The same typo that used to ship is now a red squiggle in the IDE and a CI failure on &lt;code&gt;pnpm typecheck&lt;/code&gt;. The cost was a six-line &lt;code&gt;.d.ts&lt;/code&gt; file. The payoff: adding a new role is one line in &lt;code&gt;roles.ts&lt;/code&gt;, and TypeScript tells you every controller that already references it correctly — and refuses to compile any that misspell it.&lt;/p&gt;

&lt;p&gt;Two small bits of polish that make this nicer to live with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make the augmentation visible to &lt;code&gt;tsc&lt;/code&gt;.&lt;/strong&gt; Drop a side-effect import in &lt;code&gt;src/index.ts&lt;/code&gt; (or in a barrel re-exported by it) so the file is part of the compilation unit. &lt;code&gt;tsc&lt;/code&gt; won't apply a &lt;code&gt;declare module&lt;/code&gt; it never sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the union in one file.&lt;/strong&gt; Import the role type everywhere from the same module — including inside the &lt;code&gt;mapPayload&lt;/code&gt; filter. When the union grows, adding a literal updates the filter, the augmentation, and every &lt;code&gt;@Roles&lt;/code&gt; call site in lockstep.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where @Roles doesn't belong
&lt;/h2&gt;

&lt;p&gt;The service-token controller above used &lt;code&gt;@Authenticated('service-token')&lt;/code&gt; and &lt;strong&gt;no &lt;code&gt;@Roles(...)&lt;/code&gt;&lt;/strong&gt;. That's deliberate. The synthetic service user has &lt;code&gt;roles: []&lt;/code&gt;. If you slapped &lt;code&gt;@Roles('admin')&lt;/code&gt; on a method, the check would fail — the synthetic user has none of your normal roles, by design.&lt;/p&gt;

&lt;p&gt;Privileged-channel gating is the strategy choice itself: passing the strategy &lt;em&gt;is&lt;/em&gt; the authorization. Your normal role union stays scoped to user-facing routes, which keeps two concerns from leaking into each other. If you later need richer role semantics on the privileged channel, give it its own union and its own decorator (&lt;code&gt;@ServiceRoles(...)&lt;/code&gt;) kept disjoint from the user role type on purpose.&lt;/p&gt;

&lt;p&gt;The takeaway for your own designs: not every protected route needs &lt;code&gt;@Roles&lt;/code&gt;. If the strategy can only ever produce one kind of user, the strategy &lt;em&gt;is&lt;/em&gt; the gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;The mental model fits on one index card:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick a default policy. Default-deny unless you have a very good reason.&lt;/li&gt;
&lt;li&gt;List your audiences. Each audience is one strategy with one &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Order them by which one should win when more than one could match.&lt;/li&gt;
&lt;li&gt;On controllers, pick the audience with &lt;code&gt;@Authenticated('name')&lt;/code&gt;. On &lt;code&gt;@Public()&lt;/code&gt; routes, opt out explicitly.&lt;/li&gt;
&lt;li&gt;On routes, pick the role with &lt;code&gt;@Roles(...)&lt;/code&gt; — but only when the strategy actually issues users with roles.&lt;/li&gt;
&lt;li&gt;Augment &lt;code&gt;AuthUser['roles']&lt;/code&gt; once with your real role union. Now misspellings are a build error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Auth surfaces are usually where frameworks accumulate accidental complexity, and most of the bugs that result are cheap to ship and expensive to find. KickJS keeps the surface narrow, makes the safe defaults the easy ones, and pushes role correctness from runtime into the compiler. After you've lived with the augmentation for a sprint, dropping back to "any string is a valid role" feels reckless.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/authorization.html" rel="noopener noreferrer"&gt;KickJS authorization guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs-auth" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs-auth&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>auth</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Inside KickJS Adapters — Lifecycle, Composition, and Custom Patterns</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:28:01 +0000</pubDate>
      <link>https://dev.to/forinda/inside-kickjs-adapters-lifecycle-composition-and-custom-patterns-2b8o</link>
      <guid>https://dev.to/forinda/inside-kickjs-adapters-lifecycle-composition-and-custom-patterns-2b8o</guid>
      <description>&lt;p&gt;If you have written an Express app, you already know the shape of a &lt;strong&gt;middleware&lt;/strong&gt;: a function that gets &lt;code&gt;(req, res, next)&lt;/code&gt; and runs once per request. You probably also know what a &lt;strong&gt;plugin&lt;/strong&gt; looks like — a side-effecting &lt;code&gt;register(app)&lt;/code&gt; call that mutates a global at boot. KickJS gives you a third primitive sitting between those two, and most of the framework's interesting behaviour lives there: the &lt;strong&gt;adapter&lt;/strong&gt;. An adapter is not per-request like middleware, and it is not a one-shot side-effect like a plugin. It is a typed object with a &lt;em&gt;lifecycle&lt;/em&gt;, composed in order, orchestrated by the framework so your app can stand long-lived infrastructure up and tear it down deterministically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an adapter is
&lt;/h2&gt;

&lt;p&gt;An adapter is a unit of &lt;strong&gt;composable infrastructure&lt;/strong&gt; plugged into the KickJS bootstrap pipeline. You reach for one whenever a concern needs more than a single request handler can give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It owns a &lt;strong&gt;long-lived resource&lt;/strong&gt; — a database pool, a tracer SDK, a queue client, a mailer transport.&lt;/li&gt;
&lt;li&gt;It needs to &lt;strong&gt;register tokens in DI&lt;/strong&gt; so services and controllers can &lt;code&gt;@Inject(...)&lt;/code&gt; them.&lt;/li&gt;
&lt;li&gt;It contributes &lt;strong&gt;middleware to a specific phase&lt;/strong&gt; — before global middleware, after global, after routes.&lt;/li&gt;
&lt;li&gt;It needs a &lt;strong&gt;shutdown&lt;/strong&gt; so SIGTERM doesn't drop in-flight work or leak sockets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crucial mental shift from "middleware" is that the adapter is created &lt;strong&gt;once per process&lt;/strong&gt;, not once per request. The crucial shift from "plugin" is that it has a typed contract — the &lt;code&gt;AppAdapter&lt;/code&gt; interface — and the framework calls each hook for you, in order, with a context object that exposes the DI container.&lt;/p&gt;

&lt;p&gt;You will compose multiple adapters for any non-trivial app. The pattern that scales is one adapter per concern, each in its own file, all exported as a single ordered list that the bootstrap entry imports. Construction details stay encapsulated — the entry point doesn't need to know which adapter wires up tracing or which one drains a connection pool. It just hands the array to &lt;code&gt;bootstrap()&lt;/code&gt; and lets each adapter's lifecycle run.&lt;/p&gt;

&lt;p&gt;That encapsulation is the real win. You can move an adapter between apps, you can unit-test one in isolation, and you can read your boot sequence as a list of names rather than a 200-line &lt;code&gt;index.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle, in firing order
&lt;/h2&gt;

&lt;p&gt;Every adapter that satisfies &lt;code&gt;AppAdapter&lt;/code&gt; can opt into four hooks. They fire in this order, exactly once per process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;beforeMount&lt;/code&gt;&lt;/strong&gt; — runs &lt;em&gt;before&lt;/em&gt; modules register their controllers and routes. This is the slot for adapters that need to inject middleware &lt;em&gt;before&lt;/em&gt; the route table exists, or that need to mount their own routes ahead of any user-defined handler. An OpenAPI/Swagger UI adapter is a typical example: it wires &lt;code&gt;/docs&lt;/code&gt; and &lt;code&gt;/openapi.json&lt;/code&gt; here so they sit in front of any catch-all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;beforeStart&lt;/code&gt;&lt;/strong&gt; — runs &lt;em&gt;after&lt;/em&gt; all modules have registered, &lt;em&gt;after&lt;/em&gt; routes are mounted, and &lt;em&gt;before&lt;/em&gt; the HTTP server begins listening. This is the canonical spot to construct resources and call &lt;code&gt;container.registerInstance(TOKEN, value)&lt;/code&gt;. By the time the first request arrives, every downstream &lt;code&gt;resolve()&lt;/code&gt; will see the registered value. Most of your adapter logic lives here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;middleware()&lt;/code&gt;&lt;/strong&gt; — called once at adapter-setup time and returns an array of &lt;code&gt;{ phase, handler }&lt;/code&gt; entries. The closures it returns are &lt;em&gt;captured&lt;/em&gt;, so they can read fields populated in &lt;code&gt;beforeStart&lt;/code&gt; lazily, at request time. Phases let you target the slot you actually want — &lt;code&gt;beforeGlobal&lt;/code&gt;, &lt;code&gt;afterGlobal&lt;/code&gt;, or &lt;code&gt;afterRoutes&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;onRouteMount&lt;/code&gt;&lt;/strong&gt; — fires per controller as routes are registered. Most adapters ignore it; collectors (OpenAPI metadata, route inventories) use it to walk every controller as the framework discovers it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After every adapter's &lt;code&gt;beforeStart&lt;/code&gt; finishes, the server starts listening and request handling begins. There is no &lt;code&gt;afterStart&lt;/code&gt; hook in the interface — the lifecycle splits naturally between "before listen" (boot) and "after listen" (every request that follows).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/strong&gt; — runs on SIGTERM/SIGINT. The framework calls every adapter's &lt;code&gt;shutdown()&lt;/code&gt; concurrently under &lt;code&gt;Promise.allSettled&lt;/code&gt;, so a slow drain in one adapter doesn't starve the others, and a thrown error doesn't skip later adapters' cleanup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is the killer feature. Before adapters, the typical Node app had its own &lt;code&gt;process.on('SIGTERM')&lt;/code&gt; block somewhere in &lt;code&gt;index.ts&lt;/code&gt;, papered over with a &lt;code&gt;Promise.all&lt;/code&gt; and a comment apologising for the order. With adapters, every concern that owns a resource brings its own teardown along, and the framework runs them all without you wiring it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ordering matters
&lt;/h2&gt;

&lt;p&gt;Adapters are an &lt;em&gt;ordered list&lt;/em&gt;, not a bag. Hooks fire in registration order — &lt;code&gt;beforeMount&lt;/code&gt; for adapter 0, then 1, then 2; same for &lt;code&gt;beforeStart&lt;/code&gt;. Get the order wrong and an upstream adapter hasn't published its DI token by the time a downstream adapter tries to read it, or — worse — auto-instrumentation hooks don't get applied to modules that already imported the un-patched version.&lt;/p&gt;

&lt;p&gt;A few generic rules of thumb that apply to almost every app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability first.&lt;/strong&gt; If you use OpenTelemetry auto-instrumentation, its initialisation has to run before the framework imports Express/HTTP. That means the adapter that owns it goes at index 0, and its &lt;em&gt;constructor&lt;/em&gt; (not a &lt;code&gt;beforeStart&lt;/code&gt; body) fires the init.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure (DB, secrets, config) before everything that needs it.&lt;/strong&gt; Anything that wants to resolve a connection pool from the container in a request handler has to come after the adapter that registered the pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth before feature modules.&lt;/strong&gt; A JWT-verifying middleware has to be in place before the controllers it gates start handling traffic. If your auth adapter runs late, you have a window where decorated routes pass through unchecked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doc/metadata collectors last.&lt;/strong&gt; OpenAPI generators rely on every controller having mounted, so an &lt;code&gt;onRouteMount&lt;/code&gt; collector should sit at the end of the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find yourself wanting to "just put this small thing first because it doesn't matter" — don't. The order is the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two adapter shapes: factory and class
&lt;/h2&gt;

&lt;p&gt;KickJS v5 ships two ways to build an adapter, and both produce the same shape. The framework only cares that the result satisfies &lt;code&gt;AppAdapter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;class form&lt;/strong&gt; gives you private fields, a constructor that runs early, and methods you can override or unit-test. Use it whenever your adapter has non-trivial state lifecycle, multiple resources to coordinate, or initialisation that must happen &lt;em&gt;before&lt;/em&gt; the framework runs any other code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TracingAdapter&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;AppAdapter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TracingAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TracingHandle&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TracingInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Init runs here, before bootstrap() patches express/http.&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;initTracing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AdapterContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOGGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;factory form&lt;/strong&gt;, &lt;code&gt;defineAdapter&amp;lt;TConfig&amp;gt;()&lt;/code&gt;, is much terser when the adapter is essentially "register one thing in DI, maybe drain it on shutdown." It takes a &lt;code&gt;name&lt;/code&gt; and a &lt;code&gt;build(config)&lt;/code&gt; that returns the lifecycle hooks as a plain object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailerAdapterFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailerConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MailerAdapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;beforeStart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAILER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Mailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule of thumb: factory form for "stateless registrations + maybe a shutdown"; class form when you need a constructor body, multiple private fields, or methods worth testing in isolation. Don't agonise — both are valid forever, and you can swap one for the other without changing how the framework consumes them.&lt;/p&gt;

&lt;h2&gt;
  
  
  DI registration patterns
&lt;/h2&gt;

&lt;p&gt;Adapters and the DI container are co-designed. The &lt;code&gt;AdapterContext&lt;/code&gt; you receive in &lt;code&gt;beforeMount&lt;/code&gt; and &lt;code&gt;beforeStart&lt;/code&gt; carries the container, and you have two main tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;container.registerInstance(TOKEN, value)&lt;/code&gt;&lt;/strong&gt; — bind a token to an already-constructed value. Eager, cheap, and the right answer for anything that should be a process-wide singleton: a connection pool, a mailer, a feature-flag client, a metrics emitter. Build it in &lt;code&gt;beforeStart&lt;/code&gt;, register it, move on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;container.registerFactory(TOKEN, () =&amp;gt; value, scope)&lt;/code&gt;&lt;/strong&gt; — bind a token to a &lt;em&gt;function&lt;/em&gt; the container calls at resolution time. Combined with &lt;code&gt;Scope.REQUEST&lt;/code&gt;, this is how you give every request its own short-lived value derived from per-request state. The factory closes over whatever long-lived state you need (caches, registries) and reads request context at the moment of resolution.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-request factories are the right tool whenever the value depends on something only known at request time — the active tenant, the current user's permissions, a per-request transaction. A typical pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;TENANT_DB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TENANT_DB resolved outside a tenant-scoped route&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The factory hands back a cached client keyed off the active request. Resolving outside a request frame throws with a clear message — a much better failure mode than a silent &lt;code&gt;undefined&lt;/code&gt; in a service three layers down.&lt;/p&gt;

&lt;p&gt;A useful discipline: every adapter that calls &lt;code&gt;registerInstance&lt;/code&gt; should also have a &lt;code&gt;shutdown()&lt;/code&gt; that disposes the same value. If you opened it, you drain it. If a registry caches a fleet of pools, the adapter that owns the registry calls &lt;code&gt;closeAll()&lt;/code&gt; on the way out, and it does so &lt;em&gt;before&lt;/em&gt; the lower-level resource the pools depend on is closed. Order shutdown the same way you'd order init, just in reverse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask yourself before building an adapter
&lt;/h2&gt;

&lt;p&gt;Before reaching for a new adapter, run through these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is this truly process-wide?&lt;/strong&gt; If the work is per-request, write middleware. If it's a one-shot mutation at boot with no teardown, a plugin or a plain function call may be enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What do I construct, and where?&lt;/strong&gt; Constructor (must run before the framework imports anything else) or &lt;code&gt;beforeStart&lt;/code&gt; (everything else)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What tokens do I publish in DI?&lt;/strong&gt; And does each one have a clear consumer story — eager singleton via &lt;code&gt;registerInstance&lt;/code&gt;, or per-request via &lt;code&gt;registerFactory(..., Scope.REQUEST)&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What middleware phase do I need?&lt;/strong&gt; &lt;code&gt;beforeGlobal&lt;/code&gt;, &lt;code&gt;afterGlobal&lt;/code&gt;, or &lt;code&gt;afterRoutes&lt;/code&gt;? If you can't answer this in one sentence, the phase is probably wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I drain it?&lt;/strong&gt; What's the shutdown order relative to the adapters around me? What do I leak if I skip it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get those five right and the lifecycle does the rest. Adapters are the place KickJS expects you to put the boring, important plumbing — and the more you let the lifecycle do the orchestration, the smaller and saner your &lt;code&gt;index.ts&lt;/code&gt; stays.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/adapters.html" rel="noopener noreferrer"&gt;KickJS adapters guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/lifecycle.html" rel="noopener noreferrer"&gt;KickJS lifecycle guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>adapters</category>
    </item>
    <item>
      <title>From v3 to v5 — Migrating a Production KickJS App, Slice by Slice</title>
      <dc:creator>Orinda Felix Ochieng</dc:creator>
      <pubDate>Sat, 25 Apr 2026 03:21:54 +0000</pubDate>
      <link>https://dev.to/forinda/from-v3-to-v5-migrating-a-production-kickjs-app-slice-by-slice-3ee9</link>
      <guid>https://dev.to/forinda/from-v3-to-v5-migrating-a-production-kickjs-app-slice-by-slice-3ee9</guid>
      <description>&lt;p&gt;If you maintain a KickJS v3 app, the v5 changelog probably reads like a wall of unfamiliar primitives — &lt;code&gt;defineAdapter&lt;/code&gt;, &lt;code&gt;defineHttpContextDecorator&lt;/code&gt;, &lt;code&gt;Scope.REQUEST&lt;/code&gt;, &lt;code&gt;definePlugin&lt;/code&gt;, &lt;code&gt;ContextMeta&lt;/code&gt; augmentations. The temptation is to delay: "we'll do it next quarter." This is a conceptual walkthrough of what actually changes between v3 and v5, what to migrate in what order, and where the sharp edges hide. The goal is to leave you with a mental model, not a recipe.&lt;/p&gt;

&lt;p&gt;The short version: a v5 bump is worth doing if you're still adding features. Per-request boilerplate shrinks, role decorators become typesafe, the dep tree gets smaller, and your codebase stops drifting away from where the framework's own docs and examples now live. The migration is mechanical but not trivial, and it goes much better as a planned slice-by-slice swarm than as a Saturday afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of what changes
&lt;/h2&gt;

&lt;p&gt;Three structural shifts define the v3 → v5 jump, with v4 sitting in the middle as a deprecation runway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adapters become functional.&lt;/strong&gt; In v3 every cross-cutting concern — infrastructure, auth, observability, mailer, swagger — was a class implementing an &lt;code&gt;AppAdapter&lt;/code&gt; interface with &lt;code&gt;beforeStart&lt;/code&gt; and &lt;code&gt;shutdown&lt;/code&gt; hooks. v5 introduces &lt;code&gt;defineAdapter&lt;/code&gt;, a factory style where an adapter is just an object you assemble inline. The hook surface is similar; the difference is composability. You can return a configured adapter from a function, parameterize it cleanly, and stop fighting class-instance ergonomics in tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-request context becomes typed.&lt;/strong&gt; v3 stamped values onto a request object via middleware, and you read them back through guard helpers — &lt;code&gt;requireTenant(ctx)&lt;/code&gt;, &lt;code&gt;requireUser(ctx)&lt;/code&gt; — that threw on missing values and returned &lt;code&gt;unknown&lt;/code&gt; on success. v5 introduces &lt;em&gt;contributors&lt;/em&gt; via &lt;code&gt;defineHttpContextDecorator&lt;/code&gt;. A contributor declares a key, an optional &lt;code&gt;dependsOn&lt;/code&gt; graph, and a &lt;code&gt;resolve&lt;/code&gt; function that runs once per request before the handler. Combined with a &lt;code&gt;ContextMeta&lt;/code&gt; module augmentation, &lt;code&gt;ctx.get('tenant')&lt;/code&gt; returns a fully typed value end-to-end, with the missing-value throw centralized rather than copy-pasted at the top of every handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugins replace bundled goodies.&lt;/strong&gt; v3 shipped a first-party mailer package. v5 cuts it. The same fate hits a handful of other "kickjs-*" first-party packages that are now BYO via &lt;code&gt;definePlugin&lt;/code&gt;. If you don't use those packages this is a non-event; if you do, expect to write a thin provider interface and a small plugin wrapper.&lt;/p&gt;

&lt;p&gt;Two smaller-but-loud changes round it out: a request-scoped DI factory (&lt;code&gt;Scope.REQUEST&lt;/code&gt;) that lets you inject per-request resources directly instead of wrapping them in helper closures, and an &lt;code&gt;AuthUser&lt;/code&gt; augmentation hook that finally makes role decorators like &lt;code&gt;@Roles('admin', 'worker')&lt;/code&gt; typecheck against your actual role union.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sliced migration plan
&lt;/h2&gt;

&lt;p&gt;Treat this as a one-week swarm, not a casual refactor. The slice order matters because each step depends on the &lt;em&gt;interfaces&lt;/em&gt; of the previous one — so a revert at any point produces a working app on either side of the cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write the plan first.&lt;/strong&gt; Land a planning doc in the repo with acceptance criteria per slice. The criterion that matters: "test suite green at this commit, app boots either side of the revert." This single rule prevents you from accidentally coupling slice N+1 to the internals of slice N.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Bump to the latest v4.x first.&lt;/strong&gt; Skipping straight to v5 is the single biggest mistake you can make. Roughly half of the deprecations land as v4 &lt;em&gt;warnings&lt;/em&gt;, not v5 &lt;em&gt;errors&lt;/em&gt;. By the time you're on v5 those warnings are gone, and the only signal that something's miswired is a runtime null. Sit on v4 long enough to read every warning. Then cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Land typed contributors before touching DI.&lt;/strong&gt; Contributors should ship before request-scoped factories, because a &lt;code&gt;Scope.REQUEST&lt;/code&gt; factory that resolves "the active tenant's database" reads the tenant out of the per-request context that a contributor populates. Get the contributor and its augmentation in first. Replace the in-handler guard helpers afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Migrate request-scoped resources one module at a time.&lt;/strong&gt; This is where the bulk of the visible cleanup happens. Each module's mutation paths drop their &lt;code&gt;withResource(id, deps, async (handle) =&amp;gt; ...)&lt;/code&gt; wrappers in favor of &lt;code&gt;@Autowired(RESOURCE)&lt;/code&gt; on a use-case field. One commit per module keeps reverts surgical and gives subagents (or pair programmers) clean work boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Port adapters to &lt;code&gt;defineAdapter&lt;/code&gt; after the use-cases are clean.&lt;/strong&gt; Adapters own the DI registrations the contributors and use-cases depend on, so it's easier to convert them once the consumers are already idiomatic v5. Doing it earlier means the adapter has to support both shapes simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Cut the v5 dependency bump.&lt;/strong&gt; With everything above already in v5-idiomatic shape against the v4.x compatibility shim, the actual bump is a one-line &lt;code&gt;package.json&lt;/code&gt; change plus the &lt;code&gt;AuthUser&lt;/code&gt; augmentation that types your role decorators. Boring on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Convert internal plugins last.&lt;/strong&gt; Any first-party packages or helpers you'd already extracted get wrapped as &lt;code&gt;definePlugin&lt;/code&gt; consumers, and any dropped first-party packages (like the mailer) get replaced with your own thin provider plus a plugin wrapper.&lt;/p&gt;

&lt;p&gt;A short, generic example of the contributor shape, just to anchor the mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoadCurrentUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineHttpContextDecorator&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentUser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forinda/kickjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AuthUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that augmentation, &lt;code&gt;ctx.get('currentUser')&lt;/code&gt; is typed everywhere. No guard helper, no &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dependsOn&lt;/code&gt; only resolves contributor keys.&lt;/strong&gt; A natural first instinct is to declare &lt;code&gt;dependsOn: ['user']&lt;/code&gt; on a contributor that needs the authenticated user. If &lt;code&gt;user&lt;/code&gt; is stamped by an &lt;em&gt;adapter middleware&lt;/em&gt; — which is true for most JWT integrations — the boot will crash, because &lt;code&gt;dependsOn&lt;/code&gt; walks the contributor graph and &lt;code&gt;user&lt;/code&gt; isn't in it. Adapter middleware runs before the contributor pipeline, which is the whole point of that ordering. Read the value directly off the request inside &lt;code&gt;resolve&lt;/code&gt; and skip &lt;code&gt;dependsOn&lt;/code&gt; for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deprecated mailer is louder than the docs suggest.&lt;/strong&gt; If your app sends transactional email, plan for a small extra slice: a &lt;code&gt;MailProvider&lt;/code&gt; interface, a console implementation for dev/test, a real implementation (nodemailer or your provider of choice) for staging/prod, all wrapped in &lt;code&gt;definePlugin&lt;/code&gt; so consumers register it via &lt;code&gt;bootstrap({ plugins: [...] })&lt;/code&gt;. It's roughly an afternoon of work, but it's not "free with the bump."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test fixtures need contributor registration.&lt;/strong&gt; Any test harness that boots a mini app — &lt;code&gt;createTestApp&lt;/code&gt; and friends — needs to receive contributor registrations explicitly. Forgetting this is the #1 cause of "everything works in dev, every test 400s." Note that the contributor object itself isn't what you pass; the framework exposes a &lt;code&gt;.registration&lt;/code&gt; handle for test composition. Standardize a &lt;code&gt;beforeEach&lt;/code&gt; that resets the container, otherwise registrations from a previous test leak into the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Library version skew during partial migrations.&lt;/strong&gt; If your codebase pulls in a downstream package that itself depends on KickJS, that package's peer-dep range may not yet cover v5. During the v4 staging slice this is a non-issue. After the v5 cut, you'll either need to update the downstream package or temporarily pin it. Audit your dep graph before the cut, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process-wide caches versus naive &lt;code&gt;Scope.REQUEST&lt;/code&gt; factories.&lt;/strong&gt; A &lt;code&gt;Scope.REQUEST&lt;/code&gt; factory that opens a fresh database pool per request will absolutely melt your connection pooler under load. The v5 way is a small registry — a process-wide map keyed by whatever you scope on — that hands out cached handles. Cache the &lt;em&gt;promise&lt;/em&gt;, not the resolved handle, so concurrent first-resolves don't race. Drain the registry on adapter shutdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't share working trees across parallel migration agents.&lt;/strong&gt; If you're dispatching multiple subagents (or human collaborators) to migrate modules in parallel, give each one its own git worktree. A helper-deletion job and a module-migration job sharing a tree will eventually delete a file the other one is still importing, and you'll lose half an hour figuring out why a clean build is suddenly red.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compile-time &lt;code&gt;@Roles&lt;/code&gt;.&lt;/strong&gt; Augment &lt;code&gt;AuthUser&lt;/code&gt; with your project's role union, and &lt;code&gt;@Roles('admin', 'manager')&lt;/code&gt; becomes a typecheck. &lt;code&gt;@Roles('mananger')&lt;/code&gt; becomes a compile error. This alone prevents a class of 403-in-prod bug that was historically only catchable via integration tests. It's the single change most worth shipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A smaller dependency tree.&lt;/strong&gt; Dropping the bundled mailer (and, if applicable, any other first-party packages you weren't really using) reduces install size and unlocks a &lt;code&gt;pnpm dedup&lt;/code&gt; you might have been carrying. Not glamorous, but real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idiomatic v5 patterns compound.&lt;/strong&gt; Use-case bodies stop with eight lines of plumbing and start with the actual domain logic. New engineers can read a use-case without first learning the framework's wrapper conventions. Per-request DI means future features that need a new scoped resource (a tenant cache, a request-tied audit log, a feature-flag client) plug in through the same &lt;code&gt;Scope.REQUEST&lt;/code&gt; factory shape — you write the pattern once and reuse it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better request latency on warm paths.&lt;/strong&gt; The cached request-scoped resources mean a second request for the same scope skips lookups, secret fetches, and pool warmups. Concretely, expect tens of milliseconds shaved off cold paths and a handful off warm ones. It shows up in P95.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you do this?
&lt;/h2&gt;

&lt;p&gt;If you're on v3 and shipping new feature work weekly, yes. Plan a focused week, slice the work as described above, and treat the v4 stop as non-negotiable. The win is real and the pattern carries forward to every feature you ship after.&lt;/p&gt;

&lt;p&gt;If your v3 codebase is feature-frozen — nothing new in flight, just maintenance — skip it. The migration costs more than the marginal cleanup wins if you aren't writing new use-cases.&lt;/p&gt;

&lt;p&gt;If you're starting a new app today, start on v5. Write contributors and &lt;code&gt;defineAdapter&lt;/code&gt; from day one, augment &lt;code&gt;AuthUser&lt;/code&gt; with your role union &lt;em&gt;before&lt;/em&gt; your first &lt;code&gt;@Roles&lt;/code&gt; decorator, and write a &lt;code&gt;Scope.REQUEST&lt;/code&gt; factory the first time you reach for a request-scoped resource. You'll thank yourself the first time &lt;code&gt;tsc&lt;/code&gt; catches a typo that would otherwise have shipped.&lt;/p&gt;

&lt;p&gt;Slice by slice is the only sane way through. Smaller commits would be busywork, bigger ones would make reverts catastrophic, and the v4 staging slice catches roughly half the breakage before it can become a runtime mystery. Plan it, dispatch it, and merge it when every slice tip is green.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/forinda/kick-js" rel="noopener noreferrer"&gt;KickJS framework on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/" rel="noopener noreferrer"&gt;KickJS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forinda.github.io/kick-js/guide/migration-v3-to-v4.html" rel="noopener noreferrer"&gt;KickJS migration guide (v3 → v4)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/@forinda/kickjs" rel="noopener noreferrer"&gt;&lt;code&gt;@forinda/kickjs&lt;/code&gt; on npm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kickjs</category>
      <category>typescript</category>
      <category>node</category>
      <category>migration</category>
    </item>
  </channel>
</rss>
