I built a Node.js framework. Yes, another one. But hear me out.
TL;DR
KickJS gives you NestJS-style decorators on top of Express 5 without RxJS, without Angular-style modules, and without a 30-minute setup. One command scaffolds a full DDD module with working tests.
npx @forinda/kickjs-cli new my-api
cd my-api && pnpm dev
# API running at localhost:3000 with Swagger UI at /docs
GitHub: github.com/forinda/kick-js
Why Though
Every new API project, I'd copy-paste the same setup:
- Express + TypeScript boilerplate
- Some DI solution (Inversify, tsyringe, or hand-rolled)
- Zod schemas for validation
- Swagger generation from... somewhere
- Pagination, filtering, sorting helpers
- Error handling middleware
- Test setup with supertest
That's 500+ lines before writing any business logic. NestJS solves this, but I didn't want to learn Angular patterns for a REST API.
Show Me the Code
A complete CRUD controller:
import { Controller, Get, Post, Put, Delete, Autowired } from '@forinda/kickjs-core'
import { RequestContext } from '@forinda/kickjs-http'
@Controller()
class UserController {
@Autowired() private createUser!: CreateUserUseCase
@Autowired() private listUsers!: ListUsersUseCase
@Get('/')
async list(ctx: RequestContext) {
return ctx.paginate(
(query) => this.listUsers.execute(query),
{ filterable: ['email', 'role'], sortable: ['name', 'createdAt'] },
)
}
@Post('/', { body: createUserSchema })
async create(ctx: RequestContext) {
// ctx.body is validated by Zod, typed automatically
const user = await this.createUser.execute(ctx.body)
ctx.created(user)
}
}
What happens here:
-
@Controller()registers in the DI container -
@Autowired()injects dependencies (property injection) -
@Get('/')defines the route -
{ body: createUserSchema }validates with Zod, returns 422 on failure -
ctx.paginate()handles query parsing, filtering, sorting, pagination - The same Zod schema generates the OpenAPI spec for Swagger
Zero configuration files for any of this.
The CLI Is the Star
kick g module product
Generates 18 files:
src/modules/products/
├── presentation/
│ └── product.controller.ts
├── application/
│ ├── use-cases/
│ │ ├── create-product.use-case.ts
│ │ ├── get-product.use-case.ts
│ │ ├── list-products.use-case.ts
│ │ ├── update-product.use-case.ts
│ │ └── delete-product.use-case.ts
│ └── dtos/
│ ├── create-product.dto.ts
│ ├── update-product.dto.ts
│ └── product-response.dto.ts
├── domain/
│ ├── repositories/product.repository.ts
│ ├── services/product-domain.service.ts
│ └── entities/product.entity.ts
├── infrastructure/repositories/
│ ├── drizzle-product.repository.ts
│ └── in-memory-product.repository.ts ← test stub!
├── __tests__/
│ ├── product.controller.test.ts
│ └── product.repository.test.ts
└── index.ts
The key: it generates both the ORM repository and an in-memory test stub. Tests pass immediately — no database needed.
kick g module product --repo drizzle
npx vitest run # ✅ all tests pass
Supports DDD, CQRS, REST, and minimal patterns. Works with Drizzle, Prisma, Mongoose, or custom ORMs.
Built-in Middleware (No Extra Installs)
import {
helmet, cors, requestId, requestLogger,
csrf, rateLimit,
} from '@forinda/kickjs-http'
bootstrap({
modules: [UserModule, ProductModule],
middleware: [
helmet(), // Security headers
cors({ origin: ['https://myapp.com'] }),
requestId(), // X-Request-Id
requestLogger(), // Pino: method, URL, status, duration
csrf(), // Double-submit cookie
rateLimit(), // Sliding window
express.json(),
],
})
Everything is optional. No middleware runs unless you declare it.
Vite 8 Powers Everything
No webpack. No tsup. No esbuild config. One tool:
| What | How |
|---|---|
| Dev server |
kick dev — Vite Environment Runner, HMR in ~200ms |
| Production build |
kick build — Vite library mode |
| Tests | Vitest 4 + SWC for decorator support |
| Package builds | All 19 packages built with vite build
|
HMR preserves database connections and port bindings. Save a file, Express handler swaps instantly. No restart, no reconnect.
836+ Real Tests
Not expect(true).toBe(true) stubs. Real integration tests:
import { createTestApp } from '@forinda/kickjs-testing'
import request from 'supertest'
it('GET /api/v1/users returns paginated list', async () => {
const { expressApp } = await createTestApp({
modules: [UserModule],
})
const res = await request(expressApp)
.get('/api/v1/users')
.expect(200)
expect(res.body.data).toHaveLength(2)
expect(res.body.total).toBe(2)
})
Auth middleware tests:
it('rejects expired tokens', async () => {
const token = jwt.sign({ sub: 'u1' }, SECRET, { expiresIn: '-1s' })
await request(expressApp)
.get('/api/v1/protected/me')
.set('Authorization', `Bearer ${token}`)
.expect(401)
})
10 example apps including 4 full task management APIs (Drizzle, Prisma, Mongoose) with 100+ tests each.
The 19 Packages
| Package | What |
|---|---|
core |
DI container, 20+ decorators, Pino logger |
http |
Express 5 app, RequestContext, all middleware |
config |
Zod env validation, @Value decorator |
swagger |
Auto OpenAPI from decorators + Zod |
cli |
Scaffolding, generators, custom commands |
testing |
createTestApp, createTestModule
|
auth |
JWT, API key, OAuth strategies |
prisma |
Prisma 5/6/7 adapter |
drizzle |
Drizzle query adapter |
ws |
WebSocket with @WsController
|
graphql |
@Resolver, @Query, @Mutation
|
queue |
BullMQ, RabbitMQ, Kafka |
cron |
@Cron('0 * * * *') scheduling |
mailer |
SMTP, Resend, SES |
otel |
OpenTelemetry tracing + metrics |
devtools |
Debug dashboard at /_debug
|
multi-tenant |
Header/subdomain/path resolution |
notifications |
Email, Slack, Discord, webhook |
All lockstep versioned. Install what you need.
What's Coming
Open issues on the project board:
- Graceful request draining on shutdown
- Kubernetes readiness/liveness probes
- Circuit breaker for external services
- Request-scoped DI for multi-tenant apps
- Auto-wired OpenTelemetry trace propagation
- Runtime compatibility: CI on Node 20/22/24 + Bun/Deno smoke tests
Get Started
npx @forinda/kickjs-cli new my-api
cd my-api && pnpm dev
Visit localhost:3000/docs for Swagger UI.
KickJS
A production-grade, decorator-driven Node.js framework built on Express 5 and TypeScript.
KickJS gives you the DX of NestJS — decorators, dependency injection, module system, code generators — without the weight. No RxJS, no class-transformer, no class-validator. Just TypeScript, Zod, and decorators.
Highlights
- Custom DI container — constructor and property injection, no external dependency
-
Decorator-driven —
@Controller,@Get,@Post,@Service,@Autowired,@Middleware - Zod-native validation — schemas double as OpenAPI documentation
- Vite HMR — zero-downtime hot reload in development, preserves DB/Redis/Socket connections
-
DDD generators —
kick g module usersscaffolds entity, repository, service, use-cases, DTOs, controller - Auto OpenAPI — Swagger UI and ReDoc generated from your decorators and Zod schemas
- Pluggable — adapters for database, auth, cache, swagger; schema parsers for Zod/Yup/Joi; query builders for Drizzle/Prisma/Sequelize
-
Extensible CLI — register project-specific commands in
kick.config.ts - ESM + TypeScript strict — modern stack, tree-shakeable packages
Install the CLI
…Star it, file issues, send PRs. The project board has tagged issues for all skill levels.
Top comments (0)