ExisJS

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

src/http/users/route.ts
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 }  }}
DEFAULT

[!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:

DecoratorHTTP MethodExample
@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:

DecoratorSource / Injected ValueExample
@Req()The raw ExisRequest instancegetUser(@Req() req: Request)
@Res(options?)The ExisResponse instancesend(@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 parameterhandleHost(@HostParam('tenant') tenant: string)
@Cookies(name?) / @Cookie(name?)Parsed request cookiessession(@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 stategetState(@State('tenantId') id: string)
@AppCtx()Root ExisJS Application instanceboot(@AppCtx() app: App)
@UploadedFile()Single uploaded file from multipart formupload(@UploadedFile() file: ExisFile)
@UploadedFiles()Array or map of uploaded filesupload(@UploadedFiles() files: ExisFile[])
@Next()Next middleware functionpassthrough(@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:

src/http/users/route.ts
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():

src/http/users/route.ts
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):

src/http/items/route.ts
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:

src/http/orders/route.ts
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():

src/http/avatar/route.ts
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

src/http/base-crud.controller.ts
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

src/http/users/route.ts
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.