Project Structure
New Project
Running kick new my-api scaffolds a complete project. The layout below is the default convention — paths like src/modules/, src/config/, and the entry file are configurable. Generators read kick.config.ts for the live values, so adopters who relocate or rename directories don't fight the toolchain.
my-api/ # Default layout — adopters can rearrange
├── src/
│ ├── config/
│ │ └── index.ts # Env schema (defineEnv + loadEnv)
│ ├── index.ts # Entry point — calls bootstrap()
│ └── modules/ # modules.dir in kick.config.ts (default 'src/modules')
│ ├── hello/ # Sample module
│ │ ├── hello.controller.ts
│ │ ├── hello.module.ts
│ │ └── hello.service.ts
│ └── index.ts # Exports the modules array
├── .env / .env.example
├── .prettierrc
├── AGENTS.md # Canonical multi-agent reference (Claude, Copilot, Codex, …)
├── CLAUDE.md # Thin Claude-specific layer pointing at AGENTS.md
├── kickjs-skills.md # Task-oriented skill recipes for AI agents
├── README.md
├── kick.config.ts # CLI configuration (pattern, repo, modules dir)
├── package.json
├── tsconfig.json
├── vite.config.ts # Vite config with kickjsVitePlugin()
└── vitest.config.ts # Test runner configEntry Point
// src/index.ts
import express from 'express'
import { bootstrap, helmet, cors, requestId, requestLogger } from '@forinda/kickjs'
import { modules } from './modules'
export const app = bootstrap({
modules,
apiPrefix: '/api',
defaultVersion: 1,
middleware: [
helmet(),
cors({ origin: ['https://app.example.com'] }),
requestId(),
requestLogger(),
express.json(),
],
})
// Production: start the server directly
if (process.env.NODE_ENV === 'production') {
app.start()
}Dev Mode
// vite.config.ts
import { defineConfig } from 'vite'
import { kickjsVitePlugin } from '@forinda/kickjs-vite'
import swc from 'unplugin-swc'
export default defineConfig({
plugins: [
swc.vite({ tsconfigFile: 'tsconfig.json' }),
kickjsVitePlugin({ entry: 'src/index.ts' }),
],
})pnpm kick dev # Vite HMR — instant rebuilds, preserved DB/Redis/WS state
pnpm kick build # Production build
pnpm kick start # Production server (Vite not used at runtime)Module Patterns
KickJS supports four module patterns. Set the pattern in kick.config.ts or use the --pattern flag:
kick g module users # Uses kick.config.ts pattern (default: ddd)
kick g module users --pattern minimal # Override patternThe trees below show the default layout each pattern writes under modules.dir (default src/modules). The directory roots are conventional — relocate them via kick.config.ts > modules.dir and the generator follows.
Minimal
Bare-bones controller. Perfect for prototyping.
src/modules/users/
index.ts
users.controller.tsREST
Flat structure with service and repository separation.
src/modules/users/
index.ts
users.constants.ts
users.controller.ts
users.service.ts
users.repository.ts # Interface + DI token
inmemory-users.repository.ts # Default implementation
dtos/
create-users.dto.ts
update-users.dto.ts
users-response.dto.ts
__tests__/
users.controller.test.ts
users.repository.test.tsDDD (Domain-Driven Design)
Full vertical layering with domain, application, infrastructure, and presentation layers.
src/modules/users/
index.ts
constants.ts
presentation/
users.controller.ts
application/
dtos/
create-users.dto.ts
update-users.dto.ts
users-response.dto.ts
use-cases/
create-users.use-case.ts
update-users.use-case.ts
get-users.use-case.ts
list-users.use-case.ts
delete-users.use-case.ts
domain/
entities/
users.entity.ts
value-objects/
users-id.vo.ts
repositories/
users.repository.ts # Interface only
services/
users-domain.service.ts
infrastructure/
repositories/
inmemory-users.repository.ts # Concrete implementation
__tests__/
users.controller.test.ts
users.repository.test.tsCQRS (Command Query Responsibility Segregation)
Event-driven pattern with explicit commands, queries, and domain events.
src/modules/users/
index.ts
users.constants.ts
users.controller.ts # Dispatches commands/queries
users.repository.ts # Interface
inmemory-users.repository.ts # Implementation
dtos/
create-users.dto.ts
update-users.dto.ts
users-response.dto.ts
commands/
create-users.command.ts
update-users.command.ts
delete-users.command.ts
queries/
get-users.query.ts
list-users.query.ts
events/
users-created.event.ts
users-updated.event.ts
users-deleted.event.ts
__tests__/
users.controller.test.ts
users.repository.test.tsChoosing a Pattern
| Pattern | Best for | Complexity |
|---|---|---|
| Minimal | Scripts, prototyping, learning | Low |
| REST | Standard CRUD APIs, traditional layered apps | Medium |
| DDD | Complex business logic, domain-heavy applications | High |
| CQRS | Event-driven systems, high-throughput writes | High |
Generated Module Index
Each generated module uses import.meta.glob to eagerly load decorated classes. This ensures @Service() and @Repository() decorators fire and register in the DI container without manual imports:
// DDD pattern — src/modules/users/index.ts
import { defineModule } from '@forinda/kickjs'
import { USERS_REPOSITORY } from './domain/repositories/users.repository'
import { InMemoryUsersRepository } from './infrastructure/repositories/inmemory-users.repository'
import { UsersController } from './presentation/users.controller'
// Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
import.meta.glob(
['./domain/services/**/*.ts', './application/use-cases/**/*.ts', '!./**/*.test.ts'],
{ eager: true },
)
export const UsersModule = defineModule({
name: 'UsersModule',
build: () => ({
register(container) {
container.registerFactory(USERS_REPOSITORY, () => container.resolve(InMemoryUsersRepository))
},
routes() {
return {
path: '/users',
controller: UsersController, // framework derives the router via buildRoutes()
}
},
}),
})The REST pattern uses a broader glob since files are flat:
// REST pattern — eagerly loads services and repositories
import.meta.glob(['./**/*.service.ts', './**/*.repository.ts', '!./**/*.test.ts'], { eager: true })You can also use plain side-effect imports instead of import.meta.glob if you prefer explicit imports.
Module Composition
Modules are self-contained and composed via the modules array:
// src/modules/index.ts
import type { AppModuleEntry } from '@forinda/kickjs'
import { TodoModule } from './todos'
import { OrderModule } from './orders'
// `defineModule` factories are called at the registration site —
// the invocation produces the AppModule instance bootstrap registers.
export const modules: AppModuleEntry[] = [TodoModule(), OrderModule()]Routes are mounted at /{apiPrefix}/v{version}{path}, so a module with path: '/todos' becomes /api/v1/todos.
Repository Options
All patterns (except minimal) support swapping the repository implementation:
kick g module users --repo inmemory # Default — in-memory store
kick g module users --repo prisma # Prisma ORM
kick g module users --repo drizzle # Drizzle ORMThe module's register() method binds the interface token to the implementation. Swap implementations by changing the factory target — no other code changes needed:
container.registerFactory(
USER_REPOSITORY,
() => container.resolve(InMemoryUserRepository), // ← change this line
)Testing
Tests live in __tests__/ directories colocated with the code they test:
import { describe, it, expect, beforeEach } from 'vitest'
import { Container } from '@forinda/kickjs'
import { createTestApp } from '@forinda/kickjs-testing'
describe('UserController', () => {
beforeEach(() => Container.reset())
it('lists users', async () => {
const { expressApp } = await createTestApp({ modules: [UserModule] })
const res = await request(expressApp).get('/api/v1/users')
expect(res.status).toBe(200)
})
})Run tests with pnpm test or pnpm kick test.