Boundaries & Middleware Cascading (boundary.ts)
A boundary.ts (or <name>.boundary.ts) file defines a folder-scoped request pipeline and configuration module in the ExisJS file-system routing hierarchy.
Any boundary.ts automatically applies to every route within its directory and all descendant subdirectories recursively, providing scoped CORS policies, custom headers, security guards, lifecycle hooks, route exclusions, and dependency injection scoping.
Boundaries apply the same way in development and in production builds. If a boundary.ts file fails to load, the routes beneath it are not mounted, so a broken boundary can never leave routes unprotected.
Anatomy of a Boundary
A boundary.ts consists of two distinct components:
- Configuration: Pure declarative options declared via
defineBoundary()(functional) or@Boundary()(OOP). It sets CORS, response headers, timeout limits, exclusion patterns, probe blackholing, and DI providers. - Pipeline Handlers: Functions and lifecycle hooks that run before, during, or after request execution:
| Handler Type | Signature | Description |
|---|---|---|
| Chain Step | (req, res, next) | Standard middleware step running in declaration order before route handlers |
| Guard Hook | beforeHandle(req, res) | Runs immediately before route handler execution; aborts if returning false |
| Audit Hook | afterHandle(req, res, data) | Runs immediately after route handler returns; can transform response data |
| Pipeline Wrapper | (ctx, next) / handle(ctx, next) | Onion-style wrapper around all steps and downstream routes in the boundary |
1. Functional Paradigm
Export config = defineBoundary(...), named middleware functions (req, res, next), and optionally a default export (ctx, next) as a request wrapper:
import { defineBoundary } from 'exisjs/router'import type { Request, Response, Next, BoundaryContext } from 'exisjs/router'import { UnauthorizedError } from 'exisjs/error'
// 1. Declarative Boundary Configurationexport const config = defineBoundary({ cors: { origin: ['https://admin.example.com'], credentials: true, }, headers: { 'X-Admin-Scope': 'active', }, exclude: [ '/admin/health', { path: '/admin/public/*', method: 'GET' }, ],
// Pre-handler guard hook beforeHandle(req, res) { if (!req.headers['x-admin-token']) { res.status(401).json({ error: 'Missing admin access token' }) return false // Aborts downstream execution } },
// Post-handler response transformation hook afterHandle(req, res, data) { res.setHeader('X-Audited-At', new Date().toISOString()) return { ...data, audited: true } },})
// 2. Named Chain Step Middlewareexport function verifyApiKey(req: Request, res: Response, next: Next) { if (!req.headers['x-api-key']) { throw new UnauthorizedError('Missing required API key') } next()}
// 3. Pipeline Wrapper (Default Export)export default async function (ctx: BoundaryContext, next: Next) { const start = Date.now() try { const result = await next() const duration = Date.now() - start ctx.res.setHeader('X-Response-Time', `${duration}ms`) return result } catch (err: any) { return { success: false, error: { message: err.message || 'Internal error' }, } }}2. Class-Based (OOP) Paradigm
Decorate a class with @Boundary(...). Class methods taking (req, res, next) become chain steps, beforeHandle / afterHandle serve as lifecycle hooks, and a method named handle(ctx, next) acts as the pipeline wrapper:
import { Boundary } from 'exisjs/decorators'import type { Request, Response, Next, BoundaryContext } from 'exisjs/router'import { UnauthorizedError } from 'exisjs/error'
@Boundary({ cors: { origin: ['https://admin.example.com'], credentials: true, }, headers: { 'X-Admin-Scope': 'active', }, exclude: ['/admin/health'],})export default class AdminBoundary { // Pre-handler hook beforeHandle(req: Request, res: Response) { if (!req.headers['x-admin-token']) { res.status(401).json({ error: 'Missing admin access token' }) return false } }
// Post-handler hook afterHandle(req: Request, res: Response, data: any) { res.setHeader('X-Audited-At', new Date().toISOString()) return { ...data, audited: true } }
// Named chain step verifyApiKey(req: Request, res: Response, next: Next) { if (!req.headers['x-api-key']) { throw new UnauthorizedError('Missing required API key') } next() }
// Pipeline wrapper async handle(ctx: BoundaryContext, next: Next) { const start = Date.now() const result = await next() ctx.res.setHeader('X-Response-Time', `${Date.now() - start}ms`) return result }}next() resolves with what the route sent. Returning that value (or returning nothing) sends the route's response exactly as the route sent it, with the same type and Content-Type; a Buffer stays binary. Returning a different value replaces the response, and that value is sent like a handler's return value.
Boundary Cascading & Execution Order
Boundaries cascade top-down through the folder structure:
src/http/├── boundary.ts # Root Boundary (applies to all routes)└── admin/ ├── boundary.ts # Admin Boundary (applies to /admin/*) └── users/ └── route.ts # Handled by both Root Boundary and Admin BoundaryExecution Order
- Wrappers: Outer wrappers wrap inner wrappers (onion architecture).
- Chain Steps: Root chain steps run before nested boundary chain steps.
- Guard Hooks (
beforeHandle): Run immediately before the route handler, evaluated from root boundary down to the innermost boundary. - Response Hooks (
afterHandle): Run in reverse order (innermost boundary up to root boundary).
CORS Inheritance & Precedence
CORS configuration follows a 4-tier hierarchy:
| Priority | Level | Where Defined | Description |
|---|---|---|---|
| 1 | Route Override | route.get('/', { cors: ... }) or @Cors(...) | Specific to a single endpoint; overrides all boundaries and global configs |
| 2 | Innermost Boundary | src/http/admin/boundary.ts | Scoped to subfolder tree; overrides parent boundaries |
| 3 | Parent / Root Boundary | src/http/boundary.ts | Scoped to all routes in src/http/; overrides global defaults |
| 4 | Global Configuration | exis.config.ts (cors) | Fallback baseline for endpoints without boundary or route CORS |
[!NOTE]
If a sub-boundary omits cors, it inherits the CORS settings of the nearest ancestor boundary (or exis.config.ts). Setting cors: false on a boundary explicitly disables CORS for that entire subtree.
Headers Merging
Response headers defined across boundaries are deep-merged from root to child. If header keys conflict, the innermost boundary value takes precedence:
// src/http/boundary.ts (Root)export const config = defineBoundary({ headers: { 'X-Framework': 'ExisJS', 'X-Environment': 'production' },})
// src/http/v2/boundary.ts (Nested)export const config = defineBoundary({ headers: { 'X-Api-Version': 'v2', 'X-Environment': 'v2-canary' },})
// Endpoints under /v2 receive:// X-Framework: ExisJS// X-Api-Version: v2// X-Environment: v2-canary (overwritten by nested boundary)Route Exclusion Rules
To bypass boundary middleware for specific routes (e.g. public webhooks or health checks), add rules to exclude:
import { defineBoundary } from 'exisjs/router'
export const config = defineBoundary({ exclude: [ // 1. Exact path string '/health',
// 2. Wildcard pattern matching any HTTP method '/public/*',
// 3. Path with specific HTTP method restriction { path: '/webhooks/stripe', method: 'POST' }, { path: '/docs/*', methods: ['GET', 'HEAD'] }, ],})When an incoming request matches an exclusion rule, the boundary's chain steps and beforeHandle hooks are bypassed.
Noise Suppression & Exploit Probe Blackholing
ExisJS can automatically intercept and drop automated vulnerability scanner probes (e.g. /.env, /.git, /.DS_Store, /wp-admin) at the boundary level without flooding application logs:
import { defineBoundary } from 'exisjs/router'
export const config = defineBoundary({ blockSuspiciousProbes: true, // Blackholes malicious probe paths immediately})Dependency Injection Scoping
Boundaries can register scoped providers into the IoC container for all routes within their sub-tree:
import { defineBoundary } from 'exisjs/router'import { UsersRepository } from './repository'
export const config = defineBoundary({ providers: [ ['UsersRepository', { useClass: UsersRepository }], ],})Next Steps
- Validation Guide: Building request schemas with
tex. - Dependency Injection: IoC containers, tokens, and provider lifecycles.
- Routing: File-system route parameters and handler definitions.
- Controllers: Class-based routing and parameter decorators.