Skip to main content

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.

Source facts

Repository
forinda/kickjs-skill
Last source activity
April 25, 2026 at 06:05
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
kickjs-skill
description
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.
# 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.
View on GitHub