# Kickjs Skill

> Use whenever working in or with KickJS — the decorator-driven Node.js framework on Express 5 + TypeScript. Triggers on `@forinda/kickjs*` imports, `kick.config.ts`, `kick new`/`kick g`/`kick add` commands, decorators like `@Controller` / `@Service` / `@Module`, mentions of "KickJS" or "kickjs", or files matching `*.module.ts` / `*.controller.ts` patterns. Covers two modes — adopter (writing a user app on KickJS) and contributor (working in the kickjs monorepo itself) — and auto-detects which applies. Use even when the user does not explicitly name the framework, as long as the project shape clearly matches.

- Skill: `forinda/kickjs-skill-2` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add forinda/kickjs-skill-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/forinda/kickjs-skill-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: forinda (https://skillmd.com/u/forinda)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/forinda/kickjs-skill-2

---


# KickJS Framework Skill

KickJS has strong conventions whose violation causes silent breakage — broken HMR, env values silently undefined, decorators firing at the wrong time, plugins dropped from the DI container, lockfile drift between packages. The framework's value is the conventions; the skill's job is to keep you aligned with them.

This skill operates in **two modes** and the right one depends on which repository you're in. Detect mode first, load the matching reference file, then apply the shared invariants below.

## Step 1 — Detect mode

Run these checks in order. The first match wins.

```
contributor → both pnpm-workspace.yaml AND packages/kickjs/ exist at repo root
adopter     → kick.config.ts exists, OR package.json depends on @forinda/kickjs*
neither     → don't trigger; this isn't a KickJS context, fall back to default behaviour
```

The reason contributor wins when both apply: if you're editing the framework itself, adopter conventions about "use the published `@forinda/kickjs` API" are misleading — you ARE the published API.

## Step 2 — Load the matching reference

- **Contributor mode** → read [`references/contributor.md`](references/contributor.md). Covers monorepo layout, Turbo + Vite + tsc build pipeline, pnpm + workspace deps, package add/remove flow, lockstep release scripts, BYO recipe pattern, the "only write to `@forinda/kickjs`" rule.
- **Adopter mode** → read [`references/adopter.md`](references/adopter.md). Covers `defineAdapter`/`definePlugin`, decorator usage, env wiring, module file naming for HMR, contributors vs middleware, tests via `Container.create()`, the named `app` export, BYO swaps for the 6 v5-removed wrappers.

Both modes also consult the shared invariants in Step 3 below — those apply everywhere.

## Step 3 — Shared invariants (both modes)

These are universal. Violating any of them produces silent breakage that the type system will not catch.

### `defineAdapter` / `definePlugin` factories — never class-based

Adapters and plugins are factories, not classes:

```ts
// Right
export const myAdapter = defineAdapter({
  name: 'my-adapter',
  beforeStart: ({ container }) => { /* … */ },
  shutdown: () => { /* … */ },
})

// Wrong — class-based adapter is a v3 pattern, dropped in v4
class MyAdapter implements AppAdapter { /* … */ }
```

Why: factory shape lets the framework introspect every hook ahead of time (devtools, typegen, lifecycle ordering), narrows the dependency graph for shutdown phases, and keeps the v5 BYO recipes — and the user's adapters — using the same primitives the framework uses internally.

### `@Controller()` takes no path argument

```ts
@Controller()           // Right — mount prefix comes from routes().path
@Controller('/users')   // Wrong — path arg removed in v4
```

The mount prefix comes from `routes({ path: '/users' })` in the module, not the decorator. Mixing both produces double prefixes that look correct in tests and 404 in production.

### DI tokens use slash-delimited strings, not Symbols

```ts
// Right — adopter scope
export const USER_REPO = createToken<UserRepo>('app/users/repository')

// Right — first-party uses reserved 'kick/' prefix
export const PRISMA_CLIENT = createToken<PrismaClient>('kick/prisma/Client')

// Wrong — Symbol() doesn't survive serialization, devtools, or worker boundaries
export const USER_REPO = Symbol('UserRepo')
```

Adopter projects must NEVER use the `kick/` prefix — it's reserved for the framework. Use your own scope (typically the project name or `app/`). The contributor reference covers what scope to use for new first-party packages.

### `Container.create()` for test isolation

```ts
// Right — every test gets a fresh, isolated container
beforeEach(() => {
  container = Container.create()
  container.register(/* … */)
})

// Wrong — shared singleton bleeds state between tests; reset() is incomplete
beforeEach(() => {
  Container.getInstance().reset()
})
```

Decorators fire at class-definition time and write to the global container. Tests that share `Container.getInstance()` race each other; tests that `reset()` lose decorator-registered metadata. `Container.create()` gives each test its own isolated container without losing the framework's setup.

### `getRequestValue<K>(key)` for service-level reads — never expose `setRequestValue`

```ts
// Right — services read context values via the typed helper
import { getRequestValue } from '@forinda/kickjs'
const tenant = getRequestValue('tenant')

// Wrong — internal store API, not part of the public surface
const store = requestStore.getStore()
const tenant = store?.values.get('tenant')
```

Writes flow either through `ctx.set('key', value)` inside a handler or as the return value of a `defineContextDecorator({ resolve })`. There is intentionally NO `setRequestValue` export — letting services mutate the per-request map produces ordering bugs that are expensive to debug. If you need a value to be available, contribute it via a context decorator instead.

### Context Contributors over `@Middleware()` for ctx-population

If the only job of a piece of middleware is to compute a value other code reads off `ctx`, write it as a `defineContextDecorator` (or `defineHttpContextDecorator` when HTTP-specific), not a `@Middleware()`:

```ts
// Right — typed pipeline with deps + dependsOn ordering
const LoadTenant = defineHttpContextDecorator({
  key: 'tenant',
  deps: { repo: TENANT_REPO },
  resolve: (ctx, { repo }) => repo.findById(ctx.req.headers['x-tenant-id'] as string),
})

@LoadTenant
@Get('/me')
me(ctx: RequestContext) { ctx.json(ctx.get('tenant')) }
```

Middleware still wins for short-circuiting responses, response-stream mutation, and pre-route-matching work. The split is documented in `docs/guide/context-decorators.md`.

### BYO for the 6 removed wrappers (v5+)

`@forinda/kickjs-graphql`, `-otel`, `-cron`, `-mailer`, `-multi-tenant`, `-notifications` were removed in v5. Do NOT add them as dependencies in adopter projects, do NOT reference them as published packages in contributor docs/code. Use the BYO recipes at `docs/guide/{cron,mailer,multi-tenancy,notifications,otel,graphql}.md` — each shows how to wire the upstream library through `defineAdapter`/`definePlugin` directly. The `kick add cron` (etc.) command is wired to surface the BYO guide URL instead of erroring.

### Adapters and plugins are FACTORY CALLS, not references

```ts
// Right — call the factory; each instance owns its own state (Redis client, etc.)
bootstrap({ adapters: [redisAdapter({ url: env.REDIS_URL })], plugins: [authPlugin()] })

// Wrong — passing the factory itself; the framework will not invoke it for you
bootstrap({ adapters: [redisAdapter], plugins: [authPlugin] })
```

The closure-over-config pattern is *why* `defineAdapter`/`definePlugin` exist — every adapter instance owns isolated state, and that's how shutdown hooks know which Redis client to close.

### Per-request middleware/handler execution order

Within a single request, framework order is:

```
validation (Zod from route decorators)
→ file upload (@FileUpload)
→ class-level @Middleware()
→ method-level @Middleware()
→ context contributors (sorted by dependsOn topo order)
→ handler
```

Validation always runs first — you cannot put auth `@Middleware()` "before" Zod validation by reordering decorators. If you need work to happen pre-validation, use `bootstrap({ middleware: [...] })` (global) or an adapter's `beforeRoutes` phase.

### `RequestContext` — 3 instances per request, one shared bag

Each layer (middleware → contributors → handler) sees a *different* `RequestContext` JS object. They all read/write the same AsyncLocalStorage-backed bag, but object identity differs. Two consequences:

- `ctx.foo = bar` (direct property assignment) does NOT survive across layers. The next layer's ctx is a fresh object. Always `ctx.set('foo', bar)` / `ctx.get('foo')`.
- Services reaching outside a handler use `getRequestValue('foo')` (the typed ALS reader). Reading `ctx.foo` from a service is impossible by design — services don't see `ctx`.

### DI scope rules (silent breakage if violated)

- `Container.create()` for tests as established. For runtime: scope-rule reminder.
- `@Autowired()` on properties is **lazy-resolved** (first access). `@Inject(token)` in constructors is **eager** (DI bootstrap). Cycle detection only catches eager cycles; a lazy cycle errors at first access in production.
- A singleton service cannot inject a REQUEST-scoped service. The container detects this when the singleton tries to resolve it and throws — but only at that resolve point, not at startup. Design the graph to avoid the shape (move to TRANSIENT, or pass the value explicitly).
- `createToken<T>(name)` returns a unique frozen object **by reference**. Two files calling `createToken<X>('foo')` produce two different tokens. Always `export const X = createToken<...>('x')` and import the same const everywhere.
- Interface-based bindings need manual `module.register(container)` — `@Service` / `@Repository` auto-register only the concrete class.

### `dependsOn` typos fail at boot, not at request time

`defineHttpContextDecorator({ dependsOn: ['tenent'] })` (typo) throws `MissingContributorError` at `bootstrap()`. This is intentional — bad pipelines should fail fast. Don't try to silence the error; fix the spelling.

## Step 4 — Apply mode-specific guidance

After loading the matching reference and the invariants above, apply guidance in this order when responding:

1. **Hard rules from invariants** — non-negotiable; if you're about to write code that violates one, stop and reconsider.
2. **Mode-specific patterns** from the reference file — these are conventions; deviate only when the user has clearly chosen a different path and understands the trade-off.
3. **Project-local CLAUDE.md / AGENTS.md** — every adopter and contributor project has one. Treat it as authoritative; it overrides the skill when it disagrees on substantive points (and you should mention the disagreement to the user).

## When NOT to apply this skill

- **No KickJS signals at all** — file contents are pure Express / Hono / Fastify, no `@forinda/*` imports, no `kick.config.ts`. Don't inject KickJS conventions into a non-KickJS project.
- **Pure docs / blog edits** — adjust style to the repo's tone; framework conventions don't apply to prose.
- **The user explicitly opts out** — if they say "I know this isn't idiomatic but I want X anyway", do X and skip the lecture.

## Communication style

KickJS users are technical; they're picking a decorator framework on purpose and have likely used Nest. Don't over-explain decorators or DI. Do explain the *why* behind any KickJS-specific divergence — most rules exist because some adopter hit silent breakage and we wrote the rule down.

