Skip to content

Getting Started ​

πŸ“– Reading this on GitHub? The full rendered docs live at https://kickjs.app/ β€” every ./*.md link in this page resolves there too.

Prerequisites ​

  • Node.js 20+ to run an app (@forinda/kickjs itself)
  • Node.js ^22.18.0 || >=24.11.0 to use the dev server (kick dev) β€” @forinda/kickjs-vite depends 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-api
bash
cd my-api
pnpm install

kick 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 HMR
  • src/modules/ β€” feature modules directory (configurable via modules.dir)
  • vite.config.ts β€” Vite config for HMR dev server
  • kick.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 frontmatter
  • CLAUDE.md β€” stays at the project root (Claude Code auto-loads it there); a thin pointer at .agents/AGENTS.md
  • README.md β€” project documentation

After a framework upgrade, refresh the agent files with kick g agents -f (see Generators β†’ kick g agents).

Start Development ​

pnpm dev

The 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:

bash
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 users

This 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.ts

rest 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 ​

ts
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 ​

ts
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:

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 ​

ts
// 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:

ts
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:

ts
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 routes

It 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-swagger
ts
import { 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 test

The 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 start

Next 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 via bootstrap({ 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

Released under the MIT License. Built with TypeScript β€” runs on Express, Fastify, or h3.