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.
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.
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:
| Decorator | Represents |
|---|---|
| @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.
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.
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
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
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.
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: [] }) }}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.