Getting Started β
π Reading this on GitHub? The full rendered docs live at https://kickjs.app/ β every
./*.mdlink in this page resolves there too.
Prerequisites β
- Node.js 20+ to run an app (
@forinda/kickjsitself) - Node.js
^22.18.0 || >=24.11.0to use the dev server (kick dev) β@forinda/kickjs-vitedepends on Babel 8, which is ESM-only and sets that floor. Node 20 reached end-of-life in 2026, so upgrading is recommended regardless. - pnpm (recommended), npm, yarn or bun
Create a New Project β
pnpm dlx @forinda/kickjs-cli new my-apicd my-apipnpm installkick new detects your package manager from corepack and the lockfile; --pm pnpm|npm|yarn|bun overrides it.
This scaffolds a project with the default layout β every path below is a convention configurable through kick.config.ts, not a framework requirement:
src/index.tsβ bootstrap entry with Vite HMRsrc/modules/β feature modules directory (configurable viamodules.dir)vite.config.tsβ Vite config for HMR dev serverkick.config.tsβ CLI configuration (optional).agents/AGENTS.mdβ canonical multi-agent reference (Copilot, Codex, Gemini, β¦) β conventions, patterns, gotchas.agents/GEMINI.md,.agents/COPILOT.mdβ per-agent context files.agents/skills/<slug>/SKILL.mdβ one folder per task-oriented skill (add-module,bootstrap-export,deny-list, β¦), auto-discovered by agents that read skill frontmatterCLAUDE.mdβ stays at the project root (Claude Code auto-loads it there); a thin pointer at.agents/AGENTS.mdREADME.mdβ project documentation
After a framework upgrade, refresh the agent files with kick g agents -f (see Generators β kick g agents).
Start Development β
pnpm devThe dev server starts with Vite HMR β edit any file and the server rebuilds instantly without restarting. Database connections, Redis, and WebSocket state are preserved.
Check it came up:
curl localhost:3000/health/live
# {"status":"ok","uptime":1.42}/health/live and /health/ready are built in β a liveness probe and a readiness probe that runs every adapter's onHealthCheck(). They mount at the root, outside apiPrefix, so a probe URL an orchestrator is configured against does not move when your prefix or API version does. Pass bootstrap({ health: false }) to replace them with your own.
They run inside your middleware chain
Which means app-wide auth applies to them. If you add global authentication later, exempt /health or your liveness probe starts failing.
Generate a Module β
pnpm exec kick g module usersThis generates a flat REST module under the configured modules.dir (default src/modules, override via kick.config.ts):
src/modules/users/
users.module.ts # defineModule() factory
users.controller.ts # @Controller() β HTTP routes
users.service.ts # @Service() β business logic
users.constants.ts # query config
users.repository.ts # factory + contract + DI token, one file
dtos/
create-users.dto.ts
update-users.dto.ts
users-response.dto.ts
__tests__/
users.controller.test.ts
users.repository.test.tsrest is the default pattern; pass --pattern minimal (or --minimal) for just a controller + module.
Need a real database? --repo postgres names the store in the stub's TODOs β the file and its identifiers are the same either way β or reach for the first-party @forinda/kickjs-db layer.
--pattern picks a module shape, --template picks a project
--pattern rest|minimal is a kick g module flag. Whole-project templates are chosen once at scaffold time with kick new -t rest|minimal|fullstack β fullstack gives you a server plus a typed web app in one workspace (see the typed client).
Your First Controller β
import { Controller, Get, Post, reply, type Ctx } from '@forinda/kickjs'
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
@Controller()
export class UsersController {
@Get('/')
async list(_ctx: Ctx<KickRoutes.UsersController['list']>) {
// Return-value handlers: the runtime sends this as 200 json, and
// `kick typegen` infers the response type for the typed client.
return [{ id: '1', name: 'Alice' }]
}
@Post('/', { body: createUserSchema, name: 'CreateUser' })
async create(ctx: Ctx<KickRoutes.UsersController['create']>) {
// ctx.body is validated and typed from the Zod schema
return reply(201, { id: '2', ...ctx.body })
}
}(ctx.json(...) / ctx.created(...) remain fully supported β returning the payload is what makes the response type statically inferable. See Return-Value Handlers.)
Your First Module β
import { defineModule } from '@forinda/kickjs'
import { UsersController } from './users.controller'
export const UsersModule = defineModule({
name: 'UsersModule',
build: () => ({
routes() {
return {
path: '/users',
controller: UsersController, // framework derives the router via buildRoutes()
}
},
}),
})Register it in src/modules/index.ts:
import type { AppModuleEntry } from '@forinda/kickjs'
import { UsersModule } from './users/users.module'
// `defineModule` factories are called at the registration site β
// the invocation produces the AppModule instance bootstrap registers.
export const modules: AppModuleEntry[] = [UsersModule()]Bootstrap β
// src/index.ts
import 'reflect-metadata'
import './config' // registers env schema before bootstrap
import { bootstrap } from '@forinda/kickjs'
import { modules } from './modules'
// Export the app so the Vite plugin can pick it up in dev mode.
// In production, bootstrap() auto-starts the HTTP server.
export const app = await bootstrap({ modules })Always export the app
The Vite dev plugin reads the app export to wire HMR. Skipping the export works in production but breaks kick dev β controllers won't update on file changes.
bootstrap() takes many more options (runtime, middlewares, port, cluster, securityβ¦) β the full table is the bootstrap() options reference. The separate kick.config.ts file (CLI/codegen) is documented at KickConfig.
That's it. Your API is running at http://localhost:3000/api/v1/users.
Choosing an HTTP engine β
Express is the default, but controllers, modules, DI and RequestContext are engine-neutral β swap the engine with one option and nothing else changes:
import { fastifyRuntime } from '@forinda/kickjs/fastify'
export const app = await bootstrap({ modules, runtime: fastifyRuntime() })expressRuntime() (default), fastifyRuntime() and h3Runtime() all ship in the box; h3 also has web-standard entries for edge, Bun and Deno. See HTTP Runtimes.
Point your tests at the same engine you deploy β createTestApp({ runtime }) β or a green Express suite tells you nothing about the Fastify app you ship.
Route Summary β
Opt in to a compact route table at startup with logRouteTable: true:
export const app = await bootstrap({
modules,
logRouteTable: true,
})[Application] Routes:
UsersController /api/v1/users 5 routes (2 GET, 1 POST, 1 PUT, 1 DELETE)
Total: 5 routesIt is off by default (it used to print automatically in dev). When enabled it logs at info level, so it appears at the default LOG_LEVEL but is hidden if you raise the threshold to warn/error/silent. The old logRoutesTable option still works as a deprecated alias.
Add Swagger Docs β
pnpm add @forinda/kickjs-swaggerimport { SwaggerAdapter } from '@forinda/kickjs-swagger'
export const app = await bootstrap({
modules,
adapters: [
SwaggerAdapter({
info: { title: 'My API', version: '1.0.0' },
}),
],
})Visit http://localhost:3000/docs for Swagger UI.
Run the Tests β
kick g module writes a __tests__/ folder next to the module. Run them with your test runner β the scaffold ships Vitest:
pnpm testThe generated controller test boots the module through createTestApp and asserts against real responses, so it fails if you break a route.
Production Build β
pnpm build
pnpm startNext Steps β
- Dependency Injection β learn about the DI container
- Controllers & Routes β route decorators and validation
- Middleware β class and method middleware
- Plugins β bundle modules, adapters, middleware, and DI bindings into one reusable unit with
definePlugin()and mount them viabootstrap({ plugins: [...] }) - HTTP Runtimes β run the same app on Express, Fastify or h3
- Testing β
createTestApp, DI overrides, and testing the engine you deploy - Typed Client β call the API from your frontend with response types inferred from these handlers
- Examples β see complete example applications