ExisJS

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.


DEFAULT

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:

  1. 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.
  2. Pipeline Handlers: Functions and lifecycle hooks that run before, during, or after request execution:
Handler TypeSignatureDescription
Chain Step(req, res, next)Standard middleware step running in declaration order before route handlers
Guard HookbeforeHandle(req, res)Runs immediately before route handler execution; aborts if returning false
Audit HookafterHandle(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:

src/http/admin/boundary.ts
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:

src/http/admin/boundary.ts
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 Boundary

Execution Order

  1. Wrappers: Outer wrappers wrap inner wrappers (onion architecture).
  2. Chain Steps: Root chain steps run before nested boundary chain steps.
  3. Guard Hooks (beforeHandle): Run immediately before the route handler, evaluated from root boundary down to the innermost boundary.
  4. Response Hooks (afterHandle): Run in reverse order (innermost boundary up to root boundary).

CORS Inheritance & Precedence

CORS configuration follows a 4-tier hierarchy:

PriorityLevelWhere DefinedDescription
1Route Overrideroute.get('/', { cors: ... }) or @Cors(...)Specific to a single endpoint; overrides all boundaries and global configs
2Innermost Boundarysrc/http/admin/boundary.tsScoped to subfolder tree; overrides parent boundaries
3Parent / Root Boundarysrc/http/boundary.tsScoped to all routes in src/http/; overrides global defaults
4Global Configurationexis.config.ts (cors)Fallback baseline for endpoints without boundary or route CORS
DEFAULT

[!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:

src/http/boundary.ts
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:

src/http/boundary.ts
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:

src/http/users/boundary.ts
import { defineBoundary } from 'exisjs/router'import { UsersRepository } from './repository'
export const config = defineBoundary({  providers: [    ['UsersRepository', { useClass: UsersRepository }],  ],})

Next Steps