ExisJS

Routing & Endpoints

ExisJS uses file-system routing to map directory structures directly to HTTP endpoints. Dropping a route.ts or <name>.route.ts file inside a directory registers endpoints automatically with the native routing engine.


Routing Principles

  1. Folder-Based URL Mapping: The folder structure relative to src/http/ determines the base URL path.
  2. Dual-Paradigm Support: Routes can be authored using either the Functional Paradigm (controller(), route.*) or the Class-Based (OOP) Paradigm (@Controller(), @Get(), @Post()). Choose one paradigm per project.
  3. Automatic Parameter Inference: Dynamic URL segments defined in path strings (such as /:id or /:postId) or folder names ([id]) automatically populate typed route parameters in handler contexts.

File-System Route Conventions

Directory PathURL EndpointDescription
src/http/route.ts/Root application endpoint
src/http/health/route.ts/healthStatic route endpoint
src/http/users/route.ts/usersCollection endpoint
src/http/users/[id]/route.ts/users/:idDynamic parameter route
src/http/docs/[...slug]/route.ts/docs/*slugCatch-all wildcard route
src/http/(admin)/dashboard/route.ts/dashboardRoute group (parentheses are omitted from the URL path)

Route Definitions

Basic Route Handlers

Functional Paradigm

src/http/users/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  // GET /users  listUsers: route.get('/', {    summary: 'List users',    handle: async () => {      return [        { id: '1', name: 'Alice' },        { id: '2', name: 'Bob' },      ]    },  }),
  // POST /users  createUser: route.post('/', {    summary: 'Create user',    handle: async ({ body }) => {      return { id: '3', ...body }    },  }),})

Class-Based (OOP) Paradigm

src/http/users/route.ts
import { Controller, Get, Post, Body, ApiOperation } from 'exisjs/decorators'
@Controller()export default class UsersController {  @Get('/')  @ApiOperation({ summary: 'List users' })  async listUsers() {    return [      { id: '1', name: 'Alice' },      { id: '2', name: 'Bob' },    ]  }
  @Post('/')  @ApiOperation({ summary: 'Create user' })  async createUser(@Body() body: Record<string, any>) {    return { id: '3', ...body }  }}

Supported HTTP Methods

ExisJS supports all standard HTTP methods across both paradigms:

HTTP VerbFunctional MethodOOP DecoratorDescription
GETroute.get(path, config)@Get(path, schema)Retrieve resources
POSTroute.post(path, config)@Post(path, schema)Create resources or trigger actions
PUTroute.put(path, config)@Put(path, schema)Replace existing resources
PATCHroute.patch(path, config)@Patch(path, schema)Apply partial updates
DELETEroute.delete(path, config)@Delete(path, schema)Remove resources
OPTIONSroute.options(path, config)@Options(path, schema)Describe communication options / preflight
HEADroute.head(path, config)@Head(path, schema)Retrieve headers identical to a GET request
CONNECTroute.connect(path, config)@Connect(path, schema)Establish a two-way tunnel
TRACEroute.trace(path, config)@Trace(path, schema)Loop-back testing along network path
QUERYroute.query(path, config)@Query(path, schema)Safe query method with request payload (RFC 9535)
ALLroute.all(path, config)@All(path, schema)Matches any HTTP method for the given path

Dynamic Route Parameters

Folder-Based Parameters ([param])

Creating a folder named [id] creates a parameterized route segment.

Functional Paradigm

src/http/users/[id]/route.ts
import { controller, route } from 'exisjs/router'import { tex } from 'exisjs/validator'
const UserParamsSchema = tex.object({  id: tex.string({ min: 1 }),})
export default controller({  getUser: route.get('/', {    params: UserParamsSchema,    handle: async ({ params }) => {      return { id: params.id, name: 'Alice' }    },  }),})

Class-Based (OOP) Paradigm

src/http/users/[id]/route.ts
import { Controller, Get, Param, ApiOperation } from 'exisjs/decorators'import { tex } from 'exisjs/validator'
const UserParamsSchema = tex.object({  id: tex.string({ min: 1 }),})
@Controller()export default class SingleUserController {  @Get('/', { params: UserParamsSchema })  @ApiOperation({ summary: 'Get user by ID' })  async getUser(@Param('id') id: string) {    return { id, name: 'Alice' }  }}

In-File Sub-Paths

You can also define multiple sub-paths with route parameters inside a single route.ts file:

src/http/users/route.ts
import { controller, route } from 'exisjs/router'import { tex } from 'exisjs/validator'
export default controller({  // GET /users  list: route.get('/', {    handle: async () => [{ id: '1', name: 'Alice' }],  }),
  // GET /users/:id  getById: route.get('/:id', {    params: tex.object({ id: tex.string() }),    handle: async ({ params }) => ({ id: params.id, name: 'Alice' }),  }),
  // GET /users/:id/posts/:postId  getUserPost: route.get('/:id/posts/:postId', {    params: tex.object({      id: tex.string(),      postId: tex.string(),    }),    handle: async ({ params }) => {      return { userId: params.id, postId: params.postId, title: 'Hello ExisJS' }    },  }),})

Catch-All Routes ([...slug])

Folders named [...slug] match all subsequent nested URL paths.

src/http/docs/[...slug]/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  renderDoc: route.get('/', {    handle: async ({ params }) => {      // For a request to /docs/getting-started/installation      // params.slug contains "getting-started/installation"      return { slug: params.slug }    },  }),})

Route Groups ((group))

Parenthesized directory names group routes logically without appending the folder name to the URL path:

src/http/├── (auth)/│   ├── login/│   │   └── route.ts         # Maps to /login (not /auth/login)│   └── register/│       └── route.ts         # Maps to /register (not /auth/register)└── (marketing)/    └── pricing/        └── route.ts         # Maps to /pricing (not /marketing/pricing)

Handler Execution Context (SuperContext)

In the functional paradigm, handle receives a single context object containing all request properties and utilities:

export default controller({  endpoint: route.post('/example', {    handle: async (ctx) => {      const { body, query, params, headers, req, res, app, resolve, file } = ctx      // ...    },  }),})

Context Properties

PropertyTypeDescription
ctx.bodyTBodyParsed and validated request body payload
ctx.queryTQueryParsed and validated URL query parameters
ctx.paramsTParamsExtracted route parameters
ctx.headersRecord<string, string>Request headers map
ctx.reqRequestThe underlying ExisRequest instance
ctx.resResponseThe underlying ExisResponse instance
ctx.resolve(token) => TResolves a dependency from the IoC container
ctx.fileExisFileUploaded file instance (when multipart upload is enabled)
ctx.filesExisFile[]Uploaded files array (for multiple file uploads)
ctx.stateRecord<string, any>Request-scoped custom state

Declarative Route Configuration Options

Each route handler accepts an options object supporting validation, security policies, file uploads, and timeouts:

1. Request Validation Schemas

src/http/products/route.ts
import { controller, route } from 'exisjs/router'import { tex } from 'exisjs/validator'
const ProductQuerySchema = tex.object({  page: tex.number({ coerce: true, default: 1 }),  limit: tex.number({ coerce: true, default: 20 }),})
const ProductBodySchema = tex.object({  name: tex.string({ min: 2 }),  price: tex.number({ min: 0 }),})
export default controller({  create: route.post('/', {    query: ProductQuerySchema,    body: ProductBodySchema,    handle: async ({ query, body }) => {      return { page: query.page, product: body }    },  }),})

2. Multipart File Uploads

src/http/upload/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  uploadFile: route.post('/', {    upload: {      field: 'file',      maxBytes: 5 * 1024 * 1024, // 5MB limit      mimeTypes: ['image/png', 'image/jpeg', 'application/pdf'],    },    handle: async ({ file }) => {      return {        filename: file?.filename,        size: file?.size,        mimeType: file?.mimeType,      }    },  }),})

3. Route-Specific Timeout Overrides

You can override default timeouts for long-running endpoints:

src/http/reports/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  generateReport: route.post('/annual', {    timeout: 120000, // 120 seconds in milliseconds    handle: async () => {      // Perform long-running export      return { status: 'completed' }    },  }),})

4. Route-Specific CORS Overrides

Route-level CORS configuration takes precedence over boundary and global settings:

src/http/webhooks/stripe/route.ts
import { controller, route } from 'exisjs/router'
export default controller({  stripeWebhook: route.post('/', {    cors: {      origin: 'https://hooks.stripe.com',      methods: ['POST'],    },    handle: async ({ body }) => {      return { received: true }    },  }),})

Reusing Route Types in Services

InferRouteInput and InferRouteOutput derive types from a route definition, so service methods do not need a second copy of the request shape:

src/http/users/route.ts
import { route, type InferRouteInput, type InferRouteOutput } from 'exisjs/router'import { CreateUserSchema } from './schema'import { UserService } from './service'
export const createUser = route.post('/', {  body: CreateUserSchema,  async handle({ body, resolve }) {    return resolve(UserService).create(body)  },})
// { body, query, params } as the handler receives them, after validationexport type CreateUserInput = InferRouteInput<typeof createUser>['body']// What the handler resolves toexport type CreateUserResult = InferRouteOutput<typeof createUser>

Dependency Resolution in Functional Routes

Functional routes can resolve dependencies directly from the IoC container using ctx.resolve():

src/http/users/route.ts
import { controller, route } from 'exisjs/router'import { UserService } from './service'
export default controller({  list: route.get('/', {    handle: async ({ resolve }) => {      const userService = resolve(UserService)      return userService.findAll()    },  }),})

Next Steps