Getting Started
Install exjs-controllers, set up TypeScript decorators, write your first controller and bootstrap the Express application.
Requirements
- Node.js
>=22 - TypeScript
5.2or newer - Peers —
express ^5andzod ^4, installed by your project. TypeScript projects also need@types/express.
TypeScript configuration
Parameter decorators such as @Body(), @Param() and @CurrentUser() only exist in TypeScript's legacy decorator implementation, so enable experimentalDecorators. emitDecoratorMetadata is not required: the framework never reads design:* metadata — every decorator receives what it needs explicitly (@Body(CreateUserInput), @Field(z.string())).
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"useDefineForClassFields": false
}
}TC39 decorators
Class and method decorators (@Controller, @Get, @Authorized, ...) also run under the standard TC39 implementation and detect the mode automatically. Because the standard has no parameter decorators, any project using @Body, @Param, @CurrentUser and friends still needs experimentalDecorators: true.
Project layout
configureApplication registers controllers from an explicit controllers: [...] list or auto-discovers them from one or more directories. The discovery uses the convention *.controller.ts (or .js/.mjs/...) to locate files.
src/
modules/
users/
users.controller.ts
users.service.ts
dtos/
create-user.input.ts
user.output.ts
server.tsBootstrap
import { createApplication } from 'exjs-controllers/http/application'
import { configureApplication } from 'exjs-controllers/config/configureApplication'
import { UsersController } from './modules/users/users.controller'
const app = createApplication()
await configureApplication(app, {
controllers: [UsersController],
openapi: {
documentation: {
info: { title: 'My API', version: '1.0.0' }
}
},
enableScalar: true
})
app.listen(3000, () => {
console.log('Listening on http://localhost:3000')
})createApplication() returns a typed wrapper around Express that exposes the underlying instance as app.express while keeping the surface area focused (use, get, post, put, patch, delete, listen, address, close).
Your first controller
import { z } from 'zod'
import { JsonController } from 'exjs-controllers/decorators/Controller'
import { Get, Post } from 'exjs-controllers/decorators/HttpMethod'
import { Body, Param } from 'exjs-controllers/decorators/Params'
import { Field } from 'exjs-controllers/decorators/Field'
import { BaseSchema } from 'exjs-controllers/schemas/BaseSchema'
class CreateUserInput extends BaseSchema {
@Field(z.string().min(1))
name!: string
@Field(z.string().email())
email!: string
}
class UserOutput extends BaseSchema {
@Field(z.string())
id!: string
@Field(z.string())
name!: string
@Field(z.string())
email!: string
}
@JsonController('/users')
export class UsersController {
@Get('/:id', { outputClass: UserOutput, summary: 'Get user' })
findOne(@Param('id') id: string): UserOutput {
return UserOutput.create({ id, name: 'Alice', email: 'alice@example.com' })
}
@Post('/', { inputClass: CreateUserInput, outputClass: UserOutput })
create(@Body(CreateUserInput) input: CreateUserInput): UserOutput {
input.validate()
return UserOutput.create({ id: '1', name: input.name, email: input.email })
}
}Next steps
Controllers
All controller and route decorators, with the full list of RouteOptions.
Parameters
Bind body, params, query, headers, principals and the raw Request/Response.
DTOs
BaseSchema, @Field, create/merge/validate and inheritance rules.
configureApplication
The complete ExpressServerOptions reference.