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,
middlewares: [
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 two module patterns. Set the pattern in kick.config.ts or use the --pattern flag:
kick g module users # Uses kick.config.ts pattern (default: rest)
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/
users.module.ts
users.controller.tsREST (default)
Flat structure with service and repository separation.
src/modules/users/
users.module.ts
users.constants.ts
users.controller.ts
users.service.ts
users.repository.ts # Interface + DI token
in-memory-users.repository.ts # Default implementation (in-memory)
dtos/
create-users.dto.ts
update-users.dto.ts
users-response.dto.ts
__tests__/
users.controller.test.ts
users.repository.test.tsWith a custom repo name (e.g. --repo postgres) the implementation file becomes users-postgres.repository.ts, a generic stub with TODO markers you wire to your own client.
Choosing a Pattern
| Pattern | Best for | Complexity |
|---|---|---|
| Minimal | Scripts, prototyping, learning | Low |
| REST | Standard CRUD APIs, layered service/repo apps | Medium |
Generated Module Declaration
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:
// REST pattern — src/modules/users/users.module.ts
import { defineModule } from '@forinda/kickjs'
import { USERS_REPOSITORY } from './users.constants'
import { InMemoryUsersRepository } from './in-memory-users.repository'
import { UsersController } from './users.controller'
// Eagerly load decorated classes so @Service()/@Repository() decorators register in the DI container
import.meta.glob(['./**/*.service.ts', './**/*.repository.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()
}
},
}),
})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
The REST pattern supports swapping the repository implementation by name. inmemory is the only built-in (a working in-memory store); any other name scaffolds a generic custom-repository stub with TODO markers:
kick g module users --repo inmemory # Default — working in-memory store
kick g module users --repo postgres # Generic custom-repository stub
kick g module users --repo mongo # Generic custom-repository stubThe 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.