ExisJS

Controllers

Controllers are responsible for handling incoming requests and sending responses back to the client.

A controller's purpose is to handle specific requests for the application. The routing mechanism determines which controller will handle each request. Often, a controller has multiple routes, and each route can perform a different action.

While ExisJS natively supports Functional Routing, developers coming from enterprise backgrounds often prefer Object-Oriented patterns. To create a basic OOP controller, we use classes and decorators. Decorators link classes with the necessary metadata, allowing ExisJS to automatically route requests.

Routing & Prefixes

In the following example, we'll use the @Controller() decorator, which is required to define a basic OOP controller. We'll specify an optional route path prefix of users. Using a path prefix helps group related routes together.

src/http/users/route.ts
import { Controller, Get } from 'exisjs/decorators'
@Controller('users')export default class UsersController {  @Get()  findAll(): any[] {    return [      { id: 1, name: 'Alice' },      { id: 2, name: 'Bob' }    ]  }}

The @Get() HTTP request method decorator placed before the findAll() method tells ExisJS to create a handler for GET requests. Because the controller has a prefix of users, this endpoint maps to GET /users. If we used @Get('active'), it would map to GET /users/active.

Request Object

Handlers often need access to the client’s request details. You can access the request object by injecting it using the @Req() decorator.

src/http/users/route.ts
import { Controller, Get, Req } from 'exisjs/decorators'import type { Request } from 'exisjs/router'
@Controller('users')export default class UsersController {  @Get()  findAll(@Req() request: Request): any[] {    console.log(request.ip, request.headers)    return []  }}

In most cases, you don't need to manually access the entire request object. Instead, you can use dedicated decorators provided out of the box:

DecoratorRepresents
@Req()The raw HTTP Request object
@Res()The raw HTTP Response object
@Param(key?)URL Path Parameters (e.g. req.params.id)
@Body(key?)Parsed Request Body
@Query(key?)URL Query Parameters
@Headers(name?)Specific HTTP Header
@HostParam(name?)Subdomain / Host route parameter
@Session()The request session object
@Next()Next middleware function in pipeline
@Ip()The client's IP address
@UploadedFile()A single uploaded file (multipart/form-data)
@UploadedFiles()Array of uploaded files

Payloads & Validation

For POST and PUT endpoints, you need to read the incoming JSON payload. Use the @Body() decorator to inject it.

src/http/users/route.ts
import { Controller, Post, Body } from 'exisjs/decorators'
export class CreateUserDto {  name: string;  email: string;  role: string;}
@Controller('users')export default class UsersController {  @Post()  async create(@Body() createUserDto: CreateUserDto) {    return { id: 3, ...createUserDto }  }}

Status Codes & Headers

By default, responses have a 200 OK status (except POST which uses 201). You can change this declaratively using the @HttpCode() and @Header() decorators.

src/http/users/route.ts
import { Controller, Post, HttpCode, Header } from 'exisjs/decorators'
@Controller('users')export default class UsersController {  @Post()  @HttpCode(204)  @Header('Cache-Control', 'no-store')  create() {    return null // No Content  }}

Extended Features

ExisJS provides routing enhancements like Subdomain Routing, Redirects, and Passthrough Responses. These are supported across both paradigms.

Functional Paradigm

src/http/advanced/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  // 1. Subdomain Routing  ping: route.get('/ping', {    host: ':sub.exis.local',    handle({ req }) {      return { subdomain: (req.params as any).sub }    }  }),  // 2. Redirect  redirect: route.get('/redirect', {    handle({ res }) {      res.redirect('https://google.com')    }  }),  // 3. Passthrough Response  manual: route.get('/manual', {    handle({ res }) {      res.setHeader('X-Custom', 'true')      return { msg: 'This is auto-serialized with custom headers!' }    }  })})

Class-Based (OOP) Paradigm

src/http/advanced/route.ts
import { Controller, Get, HostParam, Redirect, Res } from 'exisjs/decorators'import type { Response } from 'exisjs/router'
// 1. Subdomain Routing@Controller({ host: ':sub.exis.local' })export class SubdomainController {  @Get('/ping')  ping(@HostParam('sub') sub: string) {    return { subdomain: sub }  }}
@Controller('users')export class AdvancedController {  // 2. Redirect  @Get('/redirect')  @Redirect('https://google.com')  redirect() {}
  // 3. Passthrough Response  @Get('/manual')  manual(@Res({ passthrough: true }) res: Response) {    res.setHeader('X-Custom', 'true')    return { msg: 'This is auto-serialized with custom headers!' }  }}

Registration & Library-Specific Response

Unlike frameworks where controllers must be registered manually in a module file, ExisJS uses Folder-Based Routing. To register a controller, you export default the class inside a route.ts file.

If you need low-level control over the response, you can inject the native @Res() object.

src/http/users/route.ts
import { Controller, Get, Res } from 'exisjs/decorators'import type { Response } from 'exisjs/router'
@Controller('users')export default class UsersController {  @Get()  findAll(@Res() res: Response) {    res.status(200).json({ data: [] })  }}
Tip: Safely Returning with @Res

In some frameworks, injecting the @Res() object disables auto-serialization, causing the request to hang if you forget to call res.send(). ExisJS handles this differently: if you return a value from your handler, ExisJS will still safely auto-serialize it to JSON even if you injected @Res() to set a custom header.