Roadmap & Proposals
This document captures ideas for how to evolve KickJS. Each proposal carries enough detail to deliberate on it without re-deriving the motivation every time. DB-related items are deferred — we're working through the non-DB tracks first.
How to read this document
Each proposal uses the same template:
- Why it matters — the problem; what's broken or missing today
- What it looks like — the rough shape of the solution
- Effort — order-of-magnitude estimate (days / weeks / months)
- Open questions — things we haven't decided yet
- Status — one of:
proposed— captured, not yet discussed in depthin design— actively being shapedaccepted— we're going to build it; awaiting a phase slotbuilding— work in flightshipped— donedeferred— agreed valuable, waiting on something elserejected— decided against; kept here so we don't relitigate
Proposals are grouped into tracks by intent: the moat (differentiation), DX wins (retention), and bold bets (high-risk / high-reward).
Track A — The Moat
Things that make a developer say "I'm using KickJS specifically because nothing else does this." These are where we win — or don't — against Nest, Fastify, Hono, tRPC.
A.1 End-to-end typed client
Status: proposedEffort: 2–3 months
Why it matters. tRPC's whole pitch is full-stack type safety without a separate schema. KickJS already has the inputs (decorator-introspectable controllers, Zod schemas, the kick typegen command), but no consumer-side SDK. A developer using a kickjs backend with a Next/Remix/Vite frontend has to either hand-write API clients or pull in a separate schema layer (OpenAPI generator, gRPC, etc.) — and pay the schema-drift tax.
What it looks like.
// In the frontend:
import { kickClient } from '@forinda/kickjs-client'
import type { AppRouter } from '@server/types' // generated by `kick typegen`
const api = kickClient<AppRouter>({ baseUrl: '/api' })
const user = await api.users.create({ email: 'a@b.com' })
// ^? fully inferred response shape from the controller return type
// ^? input validated against the Zod schema attached to the controllerkick typegen --client generates a .d.ts describing every controller method, its input schema, and its return type. The runtime SDK is a thin proxy that uses HTTP under the hood (so any non-TS client — mobile, Go, curl — still works against the same REST endpoints).
Why this is the moat. Every other framework lets you handwrite a client OR hand-write an OpenAPI generator config. KickJS would be the only framework where the same decorator that defines your route also defines your client — automatically.
Open questions.
- Should the client be a separate published package, or part of
@forinda/kickjs-clitypegen output? - How do we handle streaming responses (SSE, WebSocket)? Different transport in the same client?
- Versioning: when an adopter bumps kickjs, the client must regenerate. CI hook? Watch mode?
- React/Vue/Svelte query-hook wrappers (
useQuery,useMutation) — first-party or community?
A.2 First-class observability
Status: proposedEffort: 1–2 months
Why it matters. The @forinda/kickjs-otel package exists but isn't the headline experience. Production-readiness is one of the top reasons engineering teams pick a framework — and "just install us, every controller is traced" is a story Nest doesn't have.
What it looks like.
bootstrap({
modules,
observability: {
tracer: new SentryTracerProvider({ dsn: env.SENTRY_DSN }),
metrics: new PrometheusMetricsProvider({ endpoint: '/metrics' }),
},
})Auto-instrument: every controller method, every adapter call (db, queue, cron, mailer), every guard, every contributor. Spans tagged with route, status code, latency, errors. The TracerProvider interface mirrors LoggerProvider from #265 — 5 methods, plug Sentry / Honeycomb / Datadog / OTel Collector with ~20 lines.
Why this is the moat. The pitch is "bootstrap({ observability }) and you have production-grade tracing." Every other framework requires adopter-written instrumentation.
Open questions.
- Where does the abstraction end? Do we own the W3C trace context propagation, or just the integration point?
- Sampling — built-in or delegated to the provider?
- Should metrics ship as a separate
MetricsProvideror be part ofTracerProvider? - Auto-instrumenting third-party libraries (Prisma, Drizzle queries) — opt-in or opt-out?
A.3 Runtime-portable core
Status: proposedEffort: 2–4 months (gradual)
Why it matters. Every decorator framework is locked to Node + Express (Nest, KickJS today). Hono runs everywhere — Bun, Deno, Cloudflare Workers, Vercel Edge, Fastly — and that's a real audience. Being the only decorator-driven framework that runs on edge runtimes is a genuine wedge.
What it looks like.
@forinda/kickjs-core ← no node:* imports, runtime-agnostic
@forinda/kickjs ← express adapter on top of core (today's main package)
@forinda/kickjs-hono ← hono adapter (new)
@forinda/kickjs-edge ← Fetch API adapter for CF Workers / Vercel Edge (new)
@forinda/kickjs-bun ← Bun.serve adapter (new)The peer-dep cleanup we already did (pino removal, logger interface, multer optional) is step zero — every node-specific dep we move to an adapter is a step toward this.
Why this is the moat. Cloudflare Workers + KickJS = "decorator-driven framework at the edge with sub-100ms cold starts." Nobody has this.
Open questions.
- File system / native modules — how do we handle adapters that need fs (assets, view engines)? Optional opt-in?
- The DI container uses class metadata via
reflect-metadata. Does that work on Workers? (Yes, but bundle size matters.) - Streaming responses are different across runtimes (Node
ReadablevsReadableStream) — core API needs to converge on Web Streams. - HTTP adapter API: do we re-invent Express's
req/res, or expose Fetch APIRequest/Response?
Track B — DX Wins
Things that don't make headlines but determine whether adopters stick around.
B.1 Scaffolder feature-overlay model
Status: proposedEffort: 3–6 weeks
Why it matters. Today the CLI scaffolder is template-functions-that-return-strings (packages/cli/src/generators/templates/). Powerful, type-safe, but a contributor can't see "what a scaffolded DDD-Prisma project looks like" without running kick new. Adding a new combination requires writing TS code. The combinatorial space (pattern × repo × features) keeps growing.
What it looks like. Adopt the create-vue pattern — a feature-overlay file system where each "feature" is a directory:
packages/cli/templates/
base/ # always rendered
pattern-rest/
pattern-ddd/
pattern-cqrs/
pattern-minimal/
repo-prisma/
repo-drizzle/
repo-inmemory/
repo-custom/
feature-swagger/
feature-ws/
feature-queue/
feature-devtools/
feature-auth/The scaffolder picks the user's chosen dirs and renders them in order. Code/text files are copied verbatim; JSON files (package.json, tsconfig.json) are deep-merged. New feature = new folder, no TS edits.
Why this is DX. Contributors can read a real project to understand each feature. Adopters see exactly what they'd get. Adding a new template variant goes from "edit several TS files and hope" to "drop a folder and submit a PR."
Open questions.
- Do we keep the TS-function generators for the parts that need real logic (pluralization, name casing), with file-overlay for the rest? Or commit fully to overlay?
- Conditional logic inside files (e.g. "if user picked feature X, include this line in
index.ts") — Hygen-style EJS tags? Or accept that those cases stay as TS templates? - Migration path: keep current generator working through one release, add the overlay system, deprecate the function-based one over two releases?
B.2 Error messages with "here's the fix"
Status: proposedEffort: 2–3 weeks per pass; ongoing
Why it matters. NestJS errors are infamous for being cryptic. KickJS errors today are functional but rarely tell the user how to fix the problem. The framework that has the best error messages — Rust's compiler, Elm, Astro — wins on first-impressions retention.
What it looks like.
Today:
Error: No provider for UserServiceProposed:
✖ No provider for UserService
UserService is decorated with @Service() but its module isn't
registered in bootstrap({ modules }).
Add it:
modules: [
UsersModule, ← add this
OtherModule,
…
]
Or, if UserService should be in a different module, decorate
the right module class with @Module({ providers: [UserService] }).
Docs: https://forinda.github.io/kick-js/guide/dependency-injection#registering-servicesA centralized error catalog (packages/kickjs/src/core/error-catalog.ts) keyed by error code, with structured fields: code, summary, cause, fix, docsUrl. Each throw new HttpException(...) / framework-internal error uses the catalog.
Why this is DX. First-impression metric. A developer who hits one cryptic error in an evaluation is gone; one who hits a friendly error stays.
Open questions.
- ANSI colors / box-drawing characters in error messages — assume TTY? Detect?
- Do we localize? (Almost certainly no — but worth recording the decision.)
- Integration with the devtools dashboard — surface the same hints there?
B.3 Interactive docs / WebContainers playground
Status: proposedEffort: 2–4 weeks
Why it matters. VitePress + markdown is great, but everything is static. Vue's docs let you tweak code in the page and see it run. SvelteKit, Astro, Hono — they all do this now. For a backend framework, the friction-to-evaluation is even higher (you need a terminal, Node, a port…) — letting visitors try bootstrap({ modules: [HelloModule] }) in the docs without leaving the page would be a real adoption boost.
What it looks like. StackBlitz WebContainers (or CodeSandbox CDE) embedded in the docs. The minimal-api example becomes "click to launch in browser." Tutorials become interactive: edit the code, see the response.
Why this is DX. Evaluation friction goes to zero. A blog post linking to the docs becomes "click here to play with kickjs in your browser."
Open questions.
- WebContainers only run Node (no native modules). Excludes better-sqlite3 examples. Acceptable trade-off if we have a SQLite-via-WASM fallback?
- Hosting cost — StackBlitz is free for embeds; CodeSandbox CDE has limits.
- Authoring overhead — each interactive example needs a working project. Auto-generate from the examples archive?
B.4 kick doctor command
Status: proposedEffort: 1 week
Why it matters. "It works on my machine" debugging eats hours. Common misconfigs (env not loaded, peer dep missing, typegen stale, decorators not enabled in tsconfig, prisma not generated) are all detectable.
What it looks like.
$ kick doctor
✔ Node version: 22.7.0
✔ pnpm version: 9.1.0
✔ reflect-metadata installed
✔ tsconfig has experimentalDecorators: true
✔ tsconfig has emitDecoratorMetadata: true
⚠ src/env.ts exists but isn't imported from src/index.ts
→ Without this, @Value() works via process.env fallback but ConfigService.get() returns undefined
→ Fix: add `import './env'` to the top of src/index.ts (above any bootstrap call)
✔ .kickjs/types/ is fresh (typegen ran 2 min ago)
✖ multer is used by upload route /api/upload but isn't installed
→ Fix: pnpm add multer
✔ Prisma client generated for prisma/schema.prisma
3 checks passed, 1 warning, 1 error.Why this is DX. Reduces "the framework doesn't work" support cost by ~70%. Most "doesn't work" reports are misconfigs that this command would catch.
Open questions.
- Plugin-extensible (every adapter contributes checks) or hardcoded list?
- Run-on-bootstrap variant (warn at app start, opt-out via config)?
B.5 RFC 9457 Problem Details on ctx
Status: proposedEffort: 1–2 weeks
Why it matters. Every API has the same error-shape bikeshedding conversation: should the JSON be { error: ... } or { message: ... } or { status, message } or some custom envelope? RFC 9457 — Problem Details for HTTP APIs (July 2024, supersedes RFC 7807) is the standard answer: a single canonical shape with five fields and a known content type. Adopting it gives kickjs a "standards-compliant by default" line without forcing anything — most Node frameworks make adopters reach for a library.
What it looks like.
New helper on RequestContext:
// Canonical RFC 9457 shape — type, status, title, detail, instance, plus
// arbitrary extensions
ctx.problem({
type: 'https://api.example.com/problems/out-of-credit',
status: 403,
title: 'You do not have enough credit',
detail: 'Your current balance is 30, but that costs 50.',
instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,
// extensions per §3.2
balance: 30,
})Plus typed convenience methods on the same namespace:
ctx.problem.notFound({ detail: 'User abc not found' })
ctx.problem.badRequest({ detail: '...', errors: [...] })
ctx.problem.unauthorized()
ctx.problem.forbidden()
ctx.problem.conflict({ detail: 'Email already in use' })
ctx.problem.validation(zodIssues) // serializes Zod errors into the §3.2 "errors" extensionEach method:
- Sets
Content-Type: application/problem+json - Pre-fills
statusandtitleper RFC 9457's recommendations - Lets the caller override or extend any field
HttpException gets optional type / instance / extensions fields. The framework error handler emits application/problem+json automatically when those fields are present:
throw new HttpException('Out of credit', HttpStatus.FORBIDDEN, {
type: 'https://api.example.com/problems/out-of-credit',
detail: 'Your balance is 30, but that costs 50.',
extensions: { balance: 30 },
})Optional sibling class ProblemException for adopters who want the problem fields to be required at the type level — type-safety for the new way without forcing it on existing code.
The design decision: coexistence + passive deprecation.
Only the error-shape helpers (ctx.notFound(), ctx.badRequest()) get a @deprecated JSDoc tag pointing at the ctx.problem.* equivalent — IDEs surface the strikethrough, nothing breaks at runtime, no behavior change for any existing endpoint. Adopters migrate per call site when they next touch the file.
ctx.json(data, status?) is not deprecated — it's the generic response helper for success and any non-standard shape, and stays the canonical way to send arbitrary JSON. Same for ctx.created(), ctx.noContent(), ctx.html(), ctx.download(), ctx.render(). The deprecation is scoped narrowly to the two helpers whose entire purpose is "emit an error in our custom shape" — those are the ones RFC 9457 directly replaces.
Explicitly NOT in scope:
- No
bootstrap()config knob. Every new opt-in feature adding abootstrap({ ... })config bloats the surface area for what's ultimately a metadata-format change. Adopters opt in per call site by reaching for the new helpers. - No
kick.config.tsconfig knob either. Same reasoning. - No forced migration. The framework error handler infers behavior from the data (problem fields present → problem+json; absent → existing JSON shape). Backward compatible by detection, not by config.
ctx.jsonis not touched. It's the generic JSON response helper for success and arbitrary shapes — orthogonal to the error-format question RFC 9457 answers.
Why this is DX. Standards alignment costs adopters nothing (the old way keeps working) and gives them a real "we follow RFC 9457" line for their API docs. The framework gets uniform error shapes across the ecosystem; the typed-client work in A.1 gets a standard error type to generate against.
Open questions.
- Default
typevalue when none is provided —about:blankper RFC 9457 §4.2.1, or a kickjs-specific URI scheme likehttps://forinda.github.io/kick-js/problems/{status}? - How does this interact with the
validate()middleware? It currently throws a 400 with a custom shape — auto-upgrade to problem+json with theerrors[]extension, or keep current shape and add an opt-in flag? instanceis per-occurrence URI — should the framework auto-populate it withreq.url, or always require the caller to set it explicitly?- Localization of
title/detail— out of scope for v1 (RFC 9457 §6 acknowledges i18n is the application's responsibility), but worth deciding now.
Track C — Bold Bets
High-risk, high-reward. Each of these could be the thing KickJS is famous for, if it works.
C.1 Schema-first via one decorator
Status: proposedEffort: 4–6 months
Why it matters. Today an entity is defined four times: Prisma/Drizzle schema, Zod validator, TypeScript type, OpenAPI doc. Each has its own syntax. Drift is constant. A single source of truth — declared once, projected to all four — is the holy grail.
What it looks like.
@Schema({ table: 'users' })
class User {
@Field({ primary: true, default: 'cuid' })
id!: string
@Field({ unique: true, email: true })
email!: string
@Field({ minLength: 8 })
password!: string
@Field({ nullable: true })
bio?: string
@Relation({ many: 'posts' })
posts!: Post[]
}From this one class:
- A Prisma / Drizzle / kickjs-db migration is generated
- A Zod validator is generated for incoming payloads
- TypeScript types are inferred natively
- OpenAPI schema is emitted for swagger
Why this is bold. Prisma + tRPC + Zod-OpenAPI compressed into a single declaration. Nobody has this. Done right, it's a moat the size of an ocean.
Open questions.
- Reconciling decorator metadata with actual ORM types — do we generate runtime objects or just types?
- How do we surface ORM features that don't have schema-first analogs (raw SQL, dialect-specific types)?
- Migration story — if the user adds
@Field({ unique: true }), does that auto-generate a migration? - Does this replace
@forinda/kickjs-db, or is it built on top of it?
C.2 TS compiler plugin for runtime types
Status: proposedEffort: 3–6 months
Why it matters. TypeScript's types are erased at runtime. That's why we need decorators in the first place — to recover the type info at runtime. A ts-patch / swc plugin (like ts-runtime-checks, typia, or tspl) preserves the type info, making decorators in some cases unnecessary.
What it looks like.
Today:
@Service()
class UserService {
@Autowired() private repo!: UserRepo
constructor(@Inject(LOGGER) private log: Logger) {}
}With the plugin:
class UserService {
// Type info preserved at runtime; framework infers it's @Service-able
// and that repo + log should be injected based on their declared types.
private repo: UserRepo
constructor(private log: Logger) {}
}Why this is bold. Decorator fatigue is real. A framework that automates away the decoration ceremony — while still being a decorator framework when you want to be explicit — would feel like a generational leap.
Open questions.
- Compile-time complexity. Adopters need a plugin in their
tsconfig.json/swc.config. Acceptable friction? - Build tool compatibility — does it work with Vite, esbuild, tsc, swc, Bun?
- IDE integration — do
cmd-clickand "go to definition" still work? - Source maps / debug experience?
C.3 Multi-tenant as a config flag
Status: proposedEffort: 2–3 months
Why it matters. The multi-tenant examples in the archive exist (multi-tenant-drizzle-api, multi-tenant-prisma-api, multi-tenant-mongoose-api) but they're separate templates with significant boilerplate. SaaS teams pick frameworks partly on how easy multi-tenancy is. Making it bootstrap({ multiTenant: ... }) instead of a project rewrite is a real differentiator.
What it looks like.
bootstrap({
modules,
multiTenant: {
resolver: (req) => req.headers['x-tenant-id'] as string,
scope: 'database', // 'database' | 'schema' | 'row'
audit: true,
},
})The framework wires per-request tenant resolution, scopes the DI container, switches DB connection / schema / WHERE clause automatically, and (with audit:true) logs every cross-tenant access attempt.
Why this is bold. SaaS startups flock to whatever framework makes multi-tenancy painless. Today that's nothing — Nest has nothing built-in, Prisma has Multi-schema but with caveats, Drizzle leaves it to you. KickJS could own this.
Open questions.
- How does this interact with the typed-client (#A.1)? Tenant ID baked into the client config?
- Per-tenant connection pooling — do we own that, or delegate to the ORM adapter?
- Migration story for adopters who already have a multi-tenant app — gradual adoption path?
Quick wins
Small enough to bundle into other work or do in a half-day. Listed for visibility.
| # | Idea | Effort | Status |
|---|---|---|---|
| Q.1 | @Flag('feature-name') decorator + ConfigService integration | 2 days | proposed |
| Q.2 | kick db:seed first-class command | 3 days | proposed (DB) |
| Q.3 | HTTP/2 + HTTP/3 support in the default adapter | 1 week | proposed |
| Q.4 | kick new --with auth,swagger,drizzle,docker preset bundles | 3 days | proposed |
| Q.5 | kick test:e2e wrapper around supertest + test-app | 1 week | proposed |
| Q.6 | First-party SentryLoggerProvider example snippet in docs | 1 day | proposed |
| Q.7 | kick info — print resolved versions, peer deps, runtime info | 1 day | proposed |
What we're NOT pursuing
These came up but we decided against, with reasoning preserved so we don't relitigate.
Microservices module
rejected
NestJS has one, almost nobody uses it. Complicates the framework's identity. Adopters who need microservices reach for gRPC, NATS, RabbitMQ, or temporal — not a framework-specific abstraction. If we want a story here, it's a dedicated adapter package, not a core feature.
GraphQL as a first-class peer to REST
rejected
REST is the moat. GraphQL is a feature people sometimes want, and @forinda/kickjs-graphql exists as a BYO adapter — that's the right call. Investing in GraphQL parity with REST dilutes focus. If a developer wants GraphQL-first, there are better frameworks (Pothos, GraphQL Yoga).
Community plugin registry / curated marketplace
rejected
Registries die without a maintainer. Lean on npm's existing naming convention (@kickjs-community/*, kickjs-plugin-*) and a docs page listing known-good plugins. Cheaper to maintain, no governance overhead.
DB-related proposals (deferred)
These are valid but we're not working on them yet — focus is the non-DB tracks above.
| # | Idea | Status |
|---|---|---|
| D.1 | First-class migrations CLI (kick db:migrate dev/deploy/rollback) | deferred |
| D.2 | kick db:seed first-class command | deferred |
| D.3 | Auto-generated repository methods from @Schema() class (C.1 follow-on) | deferred |
| D.4 | DB connection pool observability (auto-instrumented) | deferred |
These will get their own track once we have a clearer picture from the non-DB work.
Prioritization — current thinking
Rough order if we were optimizing for impact-per-effort:
- B.5 — RFC 9457 Problem Details on
ctx(1–2 weeks, clear spec, no breaking change, "standards-compliant" pitch) - B.2 — Error messages with fix hints (2–3 weeks, changes how the framework feels forever)
- B.4 —
kick doctorcommand (1 week, kills a huge category of support questions) - A.1 — Typed client (2–3 months, but it's the moat)
- B.1 — Scaffolder feature-overlay (3–6 weeks, contributor-friendly)
- A.2 — Observability (1–2 months, production-readiness story)
- B.3 — Interactive docs (2–4 weeks, depends on hosting cost analysis)
- A.3 — Runtime-portable core (long arc — gradual, every peer-dep cleanup advances it)
- C.1, C.2, C.3 — bold bets, sequence after the moat is in place
Open for redirection — these are starting points, not commitments.
How to propose changes
To add or amend a proposal:
- Open a PR that edits this file
- New proposals: pick the right track (A/B/C/quick win), use the template above
- Status changes: bump the
Status:line and add a one-line note explaining the bump (e.g.,Status: accepted — chosen as Phase 6 in PR #XYZ) - Rejections: move to the "What we're NOT pursuing" section with reasoning
The goal is a single document that captures what we're considering, what we decided, and why — without becoming a graveyard.