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 cats. Using a path prefix helps us group related routes together and reduces repetitive code.

src/http/cats/route.ts
import { Controller, Get } from 'exisjs/decorators'
@Controller('cats')export default class CatsController {  @Get()  findAll(): string {    return 'This action returns all cats'  }}

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 cats, this endpoint maps to GET /cats. If we used @Get('breed'), it would map to GET /cats/breed.

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/cats/route.ts
import { Controller, Get, Req } from 'exisjs/decorators'import type { Request } from 'exisjs/router'
@Controller('cats')export default class CatsController {  @Get()  findAll(@Req() request: Request): string {    console.log(request.ip, request.headers)    return 'This action returns all cats'  }}

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/cats/route.ts
import { Controller, Post, Body } from 'exisjs/decorators'
export class CreateCatDto {  name: string;  age: number;  breed: string;}
@Controller('cats')export default class CatsController {  @Post()  async create(@Body() createCatDto: CreateCatDto) {    return \`Added new cat: \${createCatDto.name}\`  }}

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/cats/route.ts
import { Controller, Post, HttpCode, Header } from 'exisjs/decorators'
@Controller('cats')export default class CatsController {  @Post()  @HttpCode(204)  @Header('Cache-Control', 'no-store')  create() {    return 'This action adds a new cat (No Content)'  }}

Advanced Features

ExisJS natively provides powerful routing enhancements baked deeply into the Radix tree engine without sacrificing performance. Features like Subdomain Routing, Redirects, and Passthrough Responses are fully supported across both paradigms.

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!' }    }  })})

Registration & Library-Specific Response

The ExisJS Difference: Unlike NestJS, where controllers must be registered manually in a app.module.ts, ExisJS uses Folder-Based Routing. To register a controller, you simply export default the class inside a route.ts file. The framework discovers it automatically!

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

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

In traditional frameworks, injecting the @Res() object disables auto-serialization, causing the request to hang if you forget to call res.send(). ExisJS is smarter! 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.