Class-Based Controllers
ExisJS provides an Object-Oriented Programming (OOP) paradigm for developers who prefer class-based architecture, TypeScript decorators, and constructor-based Dependency Injection.
Defining a Controller
To declare a class-based controller, decorate a class with @Controller() and export it as default from a route.ts or <name>.route.ts file located inside src/http/:
import { Controller, Get, Post, Body, ApiOperation } from 'exisjs/decorators'
@Controller()export default class UsersController { @Get('/') @ApiOperation({ summary: 'List all users' }) async listUsers() { return [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ] }
@Post('/') @ApiOperation({ summary: 'Create user' }) async createUser(@Body() body: { name: string; email: string }) { return { id: 3, ...body } }}[!NOTE]
Route paths defined on @Controller('/prefix') or HTTP method decorators (@Get('/subpath')) are combined relative to the directory location inside src/http/.
HTTP Method Decorators
ExisJS exposes decorators for all standard HTTP verbs:
| Decorator | HTTP Method | Example |
|---|---|---|
@Get(path?, schema?) | GET | @Get('/') |
@Post(path?, schema?) | POST | @Post('/') |
@Put(path?, schema?) | PUT | @Put('/:id') |
@Patch(path?, schema?) | PATCH | @Patch('/:id') |
@Delete(path?, schema?) | DELETE | @Delete('/:id') |
@Options(path?, schema?) | OPTIONS | @Options('/') |
@Head(path?, schema?) | HEAD | @Head('/') |
@Connect(path?, schema?) | CONNECT | @Connect('/') |
@Trace(path?, schema?) | TRACE | @Trace('/') |
@Query(path?, schema?) | QUERY | @Query('/search') |
@All(path?, schema?) | Matches any method | @All('*') |
Parameter Decorators
ExisJS provides dedicated parameter decorators to extract and inject request data directly into controller method arguments:
| Decorator | Source / Injected Value | Example |
|---|---|---|
@Req() | The raw ExisRequest instance | getUser(@Req() req: Request) |
@Res(options?) | The ExisResponse instance | send(@Res() res: Response) |
@Param(name?) | Route path parameter (req.params) | getById(@Param('id') id: string) |
@Body(schemaOrPipe?) | Parsed request body payload (req.body) | create(@Body() body: CreateDto) |
@Query(nameOrPipe?) / @QueryParam(name?) | URL query parameter (req.query) | search(@Query('q') q: string) |
@Headers(name?) | Request headers (req.headers) | auth(@Headers('authorization') token: string) |
@HostParam(name?) | Subdomain or host parameter | handleHost(@HostParam('tenant') tenant: string) |
@Cookies(name?) / @Cookie(name?) | Parsed request cookies | session(@Cookie('sid') sid: string) |
@Ip() | Client IP address (req.ip) | audit(@Ip() clientIp: string) |
@Session() | Active session state (req.session) | profile(@Session() session: any) |
@State(key?) | AsyncLocalStorage context state | getState(@State('tenantId') id: string) |
@AppCtx() | Root ExisJS Application instance | boot(@AppCtx() app: App) |
@UploadedFile() | Single uploaded file from multipart form | upload(@UploadedFile() file: ExisFile) |
@UploadedFiles() | Array or map of uploaded files | upload(@UploadedFiles() files: ExisFile[]) |
@Next() | Next middleware function | passthrough(@Next() next: NextFunction) |
Request Validation with tex Schemas
You can validate incoming request bodies and parameters by passing schemas to method decorators or directly to parameter decorators:
import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'import { tex, type ResolveSchema } from 'exisjs/validator'
export const createUserSchema = tex.object({ name: tex.string({ min: 2 }), email: tex.email(), role: tex.enum(['admin', 'member'], { default: 'member' }),})
export type CreateUserDto = ResolveSchema<typeof createUserSchema>
@Controller()export default class UsersController { @Post('/', { body: createUserSchema }) @HttpCode(201) async create(@Body() dto: CreateUserDto) { return { id: Date.now(), ...dto } }}Dependency Injection in Controllers
Controllers support dependency injection through constructor injection or property-level injection via inject():
import { Controller, Get, Param } from 'exisjs/decorators'import { inject, Inject, Optional } from 'exisjs/di'import { UserService } from './service'
@Controller()export default class UsersController { // 1. Property-level injection private userService = inject(UserService)
// 2. Constructor injection with custom tokens constructor( @Inject('DATABASE_URL') private dbUrl: string, @Optional() @Inject('METRICS') private metrics?: any ) {}
@Get('/:id') async getUser(@Param('id') id: string) { return this.userService.getUserById(id) }}Parameter Transformation with Built-In Pipes
Pipes transform and validate incoming request data before your controller method executes. If validation fails, pipes throw an appropriate HttpError (e.g., 400 Bad Request):
import { Controller, Get, Post, Param, Query, Body, UsePipes, ParseIntPipe, ParseBoolPipe, ParseUUIDPipe, ParseArrayPipe, ValidationPipe,} from 'exisjs/decorators'
@Controller('/items')export default class ItemsController { // Parses string route parameter to integer @Get('/:id') async getItem(@Param('id', ParseIntPipe) id: number) { return { id, type: typeof id } }
// Validates boolean query string and comma-separated integer array @Get('/') async listItems( @Query('active', ParseBoolPipe) active: boolean, @Query('ids', new ParseArrayPipe({ items: ParseIntPipe })) ids: number[] ) { return { active, ids } }
// Validates standard UUID format @Get('/by-uuid/:uuid') async getByUUID(@Param('uuid', ParseUUIDPipe) uuid: string) { return { uuid } }
// Method-level validation pipe @Post('/') @UsePipes(new ValidationPipe({ transform: true })) async createItem(@Body() body: any) { return { created: true, body } }}Status Codes, Headers & Redirects
You can customize response status codes, HTTP headers, and URL redirects declaratively using decorators:
import { Controller, Post, Get, HttpCode, Header, Redirect } from 'exisjs/decorators'
@Controller()export default class OrdersController { @Post('/') @HttpCode(204) @Header('Cache-Control', 'no-store') async createOrder() { return null }
@Get('/legacy-checkout') @Redirect('/checkout', 301) legacyCheckout() {}}Multipart File Uploads
Handle file uploads using the @Upload (or @UseUpload) decorator alongside @UploadedFile() and @UploadedFiles():
import { Controller, Post, Upload, UploadedFile, Body } from 'exisjs/decorators'import type { ExisFile } from 'exisjs/router'
@Controller()export default class AvatarController { @Post('/') @Upload('avatar', { maxBytes: 5 * 1024 * 1024, mimeTypes: ['image/jpeg', 'image/png', 'image/webp'], }) async uploadAvatar( @UploadedFile() file: ExisFile, @Body('caption') caption?: string ) { await file.saveToDisk('./uploads') return { filename: file.filename, size: file.size, caption, } }}Controller Inheritance
ExisJS supports Object-Oriented class inheritance. Subclasses inherit all route handlers, parameter decorators, validation rules, and guards from base controllers:
Base Generic Controller
import { Get, Delete, Param } from 'exisjs/decorators'
export abstract class BaseCrudController<T> { protected abstract service: { findAll: () => Promise<T[]> findById: (id: string) => Promise<T | null> remove: (id: string) => Promise<boolean> }
@Get('/') async findAll(): Promise<T[]> { return this.service.findAll() }
@Get('/:id') async findById(@Param('id') id: string): Promise<T | null> { return this.service.findById(id) }
@Delete('/:id') async remove(@Param('id') id: string): Promise<{ deleted: boolean }> { const deleted = await this.service.remove(id) return { deleted } }}Concrete Controller Subclass
import { Controller, Post, Body } from 'exisjs/decorators'import { BaseCrudController } from '../base-crud.controller'import { UserService, type User } from './service'
@Controller()export default class UsersController extends BaseCrudController<User> { constructor(protected readonly service: UserService) { super() }
// Add specialized endpoint @Post('/') async create(@Body() user: User) { return this.service.create(user) }}Next Steps
- Request Object: Inspect headers, query strings, and custom context on
ExisRequest. - Response Helpers: Response shaping, custom cookies, streaming, and Server-Sent Events.
- Providers & DI: Deep dive into the IoC container and custom provider lifecycles.
- Boundaries: Apply scoped middleware and guards to entire controller directories.