Kickjs framework
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.From its SKILL.md
npx -y skills add forinda/kickjs-skill --skill kickjs-frameworkAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.3 KB, ~2.5k tokens by cl100k_base, as published. Nobody here has run it
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. 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. CoversdefineAdapter/definePlugin, decorator usage, env wiring, module file naming for HMR, contributors vs middleware, tests viaContainer.create(), the namedappexport, 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:
// 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
@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
// 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
// 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
// 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():
// 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
// 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. Alwaysctx.set('foo', bar)/ctx.get('foo').- Services reaching outside a handler use
getRequestValue('foo')(the typed ALS reader). Readingctx.foofrom a service is impossible by design — services don't seectx.
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 callingcreateToken<X>('foo')produce two different tokens. Alwaysexport const X = createToken<...>('x')and import the same const everywhere.- Interface-based bindings need manual
module.register(container)—@Service/@Repositoryauto-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:
- Hard rules from invariants — non-negotiable; if you're about to write code that violates one, stop and reconsider.
- 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.
- 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, nokick.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.
What ships with it: 2 files
37.0 KB alongside SKILL.md
references/
- adopter.md23.0 KB
- contributor.md14.0 KB