DEV Community

Cover image for KickJS: Decorator-Driven APIs on Express 5 — NestJS Ergonomics, Zero Overhead
Orinda Felix Ochieng
Orinda Felix Ochieng

Posted on

KickJS: Decorator-Driven APIs on Express 5 — NestJS Ergonomics, Zero Overhead

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
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/forinda/kick-js


Why Though

Every new API project, I'd copy-paste the same setup:

  1. Express + TypeScript boilerplate
  2. Some DI solution (Inversify, tsyringe, or hand-rolled)
  3. Zod schemas for validation
  4. Swagger generation from... somewhere
  5. Pagination, filtering, sorting helpers
  6. Error handling middleware
  7. 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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
  ],
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Visit localhost:3000/docs for Swagger UI.

GitHub logo forinda / kick-js

A declarative progressive backend framework

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 generatorskick g module users scaffolds 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.

Docs: forinda.github.io/kick-js

Top comments (0)