Routing
Routes are responsible for handling incoming requests and sending responses back to the client. The routing mechanism determines which handler will process each request.
ExisJS natively supports both a Functional Paradigm and a Class-Based OOP Paradigm (for dependency injection and decorators). You can mix and match them throughout your application.
Folder-Based Routing
ExisJS uses folder-based file-system routing instead of programmatic route declaration in a single entry file.
For a folder to become an accessible API endpoint, it must contain a route.ts file that exports a route definition.
In addition to route.ts, the ExisJS router utilizes two other special gatekeeper files:
server.ts: Placed at the root of yourhttpdirectory, it is the entry point that boots your application, registers global plugins, and connects to databases.gateway.ts: Can be placed in any folder. It acts as a gatekeeper, automatically applying middleware, CORS, headers, and dependencies to all routes within its directory and subdirectories.
Routing Conventions
The ExisJS file-system router natively supports advanced folder naming conventions to handle dynamic segments and logical grouping:
- Dynamic Parameters (
[param]): A folder named[id]creates a dynamic route segment (e.g.,/:id). The value is accessible in your handler viactx.params.id. - Catch-all Routes (
[...param]): A folder named[...slug]catches all subsequent URL segments, matching anything that follows (e.g.,/*slug). - Route Groups (
(group)): A folder wrapped in parentheses (e.g.,(admin)) is ignored in the final URL path. This is useful for grouping related routes or applying shared controller-level middleware without adding/adminto the URL.
The Application Entry Point (server.ts)
The server.ts file acts as the root configuration for your application. It provides lifecycle hooks to boot databases, register global plugins, and provide root-level dependencies.
Functional Paradigm
import { exis } from 'exisjs'
export default exis({ async onStart(app) { // Boot your database here! }})Class-Based (OOP) Paradigm
import { Server } from 'exisjs/decorators'import type { App } from 'exisjs'
@Server({ providers: [['LoggerService', { useValue: console }]]})export default class RootServer { async onStart(app: App) { // Boot your database here! }}Folder Gateways (gateway.ts)
Gateways apply configurations to all routes in their directory and subdirectories.
Functional Paradigm
import { defineGateway } from 'exisjs/router'
export default defineGateway({ headers: { 'X-Admin-Area': 'true' }})Class-Based (OOP) Paradigm
import { Gateway } from 'exisjs/decorators'
@Gateway({ headers: { 'X-Admin-Area': 'true' }})export default class AdminGateway {}Route Definitions (route.ts)
For example, to create a simple /health endpoint, you create the folder src/http/health/ and place a route.ts file inside it:
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ check: route.get('/', { handle() { return { status: 'ok', timestamp: new Date().toISOString() } } })})Class-Based (OOP) Paradigm
import { Controller, Get } from 'exisjs/decorators'
@Controller()export default class HealthController { @Get('/') check() { return { status: 'ok', timestamp: new Date().toISOString() } }}Supported Methods
ExisJS natively exposes standard HTTP methods alongside advanced routing paradigms like WebSockets and Server-Sent Events. Choose your paradigm below to see the available methods.
Functional Paradigm
| Method | Description |
|---|---|
| route.get() / post() / put() | Standard HTTP methods. Also includes patch, delete, options, head, connect, and trace. |
| route.all() | Matches any HTTP request method for the given path. |
| route.query() | The new experimental HTTP QUERY method, solving complex idempotent fetches. |
| route.ws() | Exposes a native WebSocket endpoint with automatic pub/sub support. |
| route.sse() | Exposes a Server-Sent Events stream for one-way realtime data. |
import { controller, route } from 'exisjs/router'
export default controller({ list: route.get('/', { handle() { // Fetch all users from database return [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] } }), create: route.post('/', { handle({ req }) { // Create a new user with parsed payload const payload = req.body return { id: 3, ...payload } } }), preflight: route.options('/', { handle({ res }) { // Handle custom preflight logic if `preflightContinue: true` is configured in CORS res.header('X-Custom-Options', 'true') return { allowed: true } } })})Class-Based (OOP) Paradigm
| Decorator | Description |
|---|---|
| @Get() / @Post() / @Put() | Standard HTTP methods. Also includes @Patch, @Delete, @Options, @Head, @Connect, and @Trace. |
| @All() | Matches any HTTP request method for the given path. |
| @Query() | The new experimental HTTP QUERY method, solving complex idempotent fetches. |
| @Ws() | Exposes a native WebSocket endpoint with automatic pub/sub support. |
| @Sse() | Exposes a Server-Sent Events stream for one-way realtime data. |
import { Controller, Get, Post, Body } from 'exisjs/decorators'
@Controller()export default class UsersController { @Get('/') list() { // Fetch all users from database return [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ] }
@Post('/') create(@Body() data: any) { // Create a new user with parsed payload return { id: 3, ...data } }}Sub-Paths in a Single File
While folder-based routing is standard, you can also define multiple sub-paths in a single route.ts file to group related endpoints.
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ list: route.get('/', { handle() { return { message: 'Fetching all users' } } }), getSingle: route.get('/:id', { handle({ params }) { const { id } = params return { message: \`Fetching user \${id}\` } } })})Class-Based (OOP) Paradigm
import { Controller, Get, Param } from 'exisjs/decorators'
@Controller()export default class UsersController { @Get('/') list() { return { message: 'Fetching all users' } }
@Get('/:id') getSingle(@Param('id') id: string) { return { message: \`Fetching user \${id}\` } }}Route Parameters (Dynamic Routing)
Routes with static paths won’t work when you need to accept dynamic data as part of the request (e.g., GET /users/123). To create dynamic routes using folders, enclose the folder name in brackets [].
Functional Paradigm
// Location: src/http/users/[id]/route.tsimport { controller, route } from 'exisjs/router'
export default controller({ getSingle: route.get('/', { handle({ params }) { const { id } = params return { message: \`Fetching user \${id}\` } } })})Class-Based (OOP) Paradigm
// Location: src/http/users/[id]/route.tsimport { Controller, Get, Param } from 'exisjs/decorators'
@Controller()export default class SingleUserController { @Get('/') getSingle(@Param('id') id: string) { return { message: \`Fetching user \${id}\` } }}Request & Response Objects
Handlers often need access to the client’s request details, or fine-grained control over the response headers and status codes. Every route handler natively receives powerful req and res objects.
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ profile: route.get('/profile', { handle({ req, res }) { // Read Request details const ip = req.ip const headers = req.headers
// Chain Response methods res .status(202) .cookie('session', 'xyz123', { httpOnly: true, secure: true }) .header('X-Custom-Header', 'Exis') .json({ message: 'Accepted' }) } })})Class-Based (OOP) Paradigm
import { Controller, Get, Req, Res } from 'exisjs/decorators'import type { Request, Response } from 'exisjs/router'
@Controller()export default class ProfileController { @Get('/profile') profile(@Req() req: Request, @Res() res: Response) { // Read Request details const ip = req.ip const headers = req.headers
// Chain Response methods res .status(202) .cookie('session', 'xyz123', { httpOnly: true, secure: true }) .header('X-Custom-Header', 'Exis') .json({ message: 'Accepted' }) }}When a request handler returns a JavaScript object or array, it is automatically serialized to JSON and sent with a default 200 OK status code. You only need to use the res object when you want to explicitly modify headers, cookies, or status codes!
Payloads & Query Parameters
You can easily extract query strings from the URL or parsed JSON bodies from POST requests directly through the req object (or via decorators in OOP).
Functional Paradigm
import { controller, route } from 'exisjs/router'
export default controller({ search: route.get('/search', { handle({ req }) { // e.g., GET /search?q=exis&limit=10 const { q, limit } = req.query return { query: q, limit } } }), create: route.post('/create', { handle({ req }) { // Access parsed JSON body natively const data = req.body return { received: data } } })})Class-Based (OOP) Paradigm
import { Controller, Get, Post, Query, Body } from 'exisjs/decorators'
@Controller()export default class SearchController { @Get('/search') search(@Query('q') q: string, @Query('limit') limit: number) { // e.g., GET /search?q=exis&limit=10 return { query: q, limit } }
@Post('/create') create(@Body() data: any) { // Access parsed JSON body natively return { received: data } }}Dependency Injection in Functional Controllers
ExisJS natively supports Request-Scoped Dependency Injection directly inside functional routes. You don't need to use classes to get the benefits of DI!
By destructuring resolve from the context, you can dynamically fetch services without relying on global imports. This perfectly solves the "Fat Handler" problem by encouraging you to abstract business logic into clean Service classes, and makes unit testing incredibly easy.
import { controller, route } from 'exisjs/router'import { UsersService } from './service'
export default controller({ list: route.get('/', { async handle({ resolve }) { // 1. Resolves UsersService from the IoC Container // 2. Maintains request-scope (the same instance is returned throughout this HTTP request) const usersService = resolve(UsersService) return usersService.getAll() } })})The Context API (Advanced)
For applications where passing (req, res) arguments through multiple service layers becomes tedious, ExisJS offers a Next.js-inspired approach: The Context API. Powered by Node.js AsyncLocalStorage, ExisJS provides hooks to access the current request state from anywhere in the execution context.
Functional Paradigm
import { controller, route, getContext, getRequest, getResponse, after } from 'exisjs/router'
export default controller({ create: route.post('/', { async handle() { // Access request/response without prop-drilling const req = getRequest() const res = getResponse() // Access data injected by gateways const { userId } = getContext<{ userId: string }>() // Run background tasks after sending response to client after(async () => { await sendNotificationEmail(userId) })
res.status(201) return { success: true } } })})Class-Based (OOP) Paradigm
import { Controller, Post } from 'exisjs/decorators'import { getContext, getRequest, getResponse, after } from 'exisjs/router'
@Controller()export default class OrdersController { @Post('/') async create() { // You can still use the Context API seamlessly in OOP classes! const req = getRequest() const res = getResponse() const { userId } = getContext<{ userId: string }>() after(async () => { await sendNotificationEmail(userId) })
res.status(201) return { success: true } }}Modular Architecture
While putting all logic in route.ts is fine for small apps, ExisJS strongly encourages separating concerns alongside your route definition:
schema.ts: Defines expected input validation constraints.service.ts: Pure functions that interact with your database.controller.ts: Handles business orchestration.route.ts: Wires it to the HTTP network.