exisjs/context
The exisjs/context primitives are built on Node.js AsyncLocalStorage (node:async_hooks). They provide request-scoped contextual state across asynchronous call stacks without manual parameter passing through deep service, repository, or DataLoader layers.
Module Exports
Context functions are accessible from exisjs/router and exisjs/app:
import { getContext, setContext, getRequest, getResponse, after,} from 'exisjs/router'
import { getActiveApp, getApp } from 'exisjs/app'import type { Request, Response } from 'exisjs/router'API Reference
getContext<T>()
Retrieves the mutable request state dictionary stored in the active asynchronous execution context.
function getContext<T = Record<string, any>>(): TReturns
| Type | Description |
|---|---|
T | The typed request state object attached to the current asynchronous execution context. |
Throws
Throws an Error if invoked outside of an active HTTP request lifecycle.
Example
import { getContext } from 'exisjs/router'
interface TenantState { tenantId: string userId: string roles: string[]}
export function getCurrentTenant(): string { const state = getContext<TenantState>() return state.tenantId}setContext(key, value)
Stores a key-value entry into the current request state dictionary.
function setContext(key: string, value: any): voidParameters
| Parameter | Type | Description |
|---|---|---|
key | string | The state key identifier. |
value | any | The value to store in the context. |
Example
import { defineBoundary } from 'exisjs/router'import { setContext } from 'exisjs/router'
export default defineBoundary({ async handle({ req, next }) { const tenantId = (req.headers['x-tenant-id'] as string) || 'default' setContext('tenantId', tenantId) return next() },})getRequest()
Retrieves the active HTTP Request object from the asynchronous execution context.
function getRequest(): RequestReturns
| Type | Description |
|---|---|
Request | The current request instance containing headers, params, query, body, ip, and contextual properties. |
Example
import { Dataloader } from 'exisjs/dataloader'import { getRequest } from 'exisjs/router'
export const userLoader = new Dataloader(async (userIds: readonly string[]) => { const req = getRequest() const traceId = req.headers['x-trace-id']
return db.users.findMany({ where: { id: { in: [...userIds] } }, metadata: { traceId }, })})getResponse()
Retrieves the active HTTP Response object from the asynchronous execution context.
function getResponse(): ResponseReturns
| Type | Description |
|---|---|
Response | The current response instance containing response headers, status codes, and streaming controls. |
Example
import { getResponse } from 'exisjs/router'
export function clearAuthCookie(): void { const res = getResponse() res.clearCookie('access_token', { path: '/', httpOnly: true, secure: process.env.NODE_ENV === 'production', })}after(callback)
Registers an asynchronous callback to be executed in the background after the HTTP response has been sent to the client.
function after(callback: () => void | Promise<void>): voidParameters
| Parameter | Type | Description |
|---|---|---|
callback | () => void | Promise<void> | Non-blocking function to execute after the response finishes. |
Behavior
- Callbacks run asynchronously without delaying the client's HTTP response.
- If a callback rejects or throws an error, the error is caught and forwarded to the application logger (
logger.error). It will not crash the Node.js process. - All queued callbacks are drained deterministically during request context cleanup.
Example
import { controller, route } from 'exisjs/router'import { after } from 'exisjs/router'import { tex } from 'exisjs/validator'import { sendOrderConfirmationEmail } from '../../services/mail'
export default controller({ create: route.post('/', { body: tex.object({ orderId: tex.string(), email: tex.email(), }), async handle({ body }) { const order = await db.orders.create({ data: body })
// Queue background job after client receives 201 response after(async () => { await sendOrderConfirmationEmail(order.id, body.email) })
return { success: true, orderId: order.id } }, }),})getActiveApp() / getApp()
Retrieves the active ExisJS App instance processing the current request lifecycle.
function getActiveApp(): Appfunction getApp(): AppReturns
| Type | Description |
|---|---|
App | The running application instance containing the DI container (app.container), logger (app.log), and configuration. |
Example
import { getActiveApp } from 'exisjs/app'
export function logAuditEvent(action: string, payload: Record<string, any>): void { const app = getActiveApp() app.log.info({ action, payload }, 'Audit event recorded')}Lifecycle & Memory Safety
ExisJS implements deterministic context lifecycle management to prevent memory retention in long-lived or high-concurrency environments:
Incoming Request │ ▼ExecutionContext Initialized (AsyncLocalStorage.run) │ ├─► Middleware & Boundaries (setContext) ├─► Route Handler Execution (getRequest / getResponse / getContext) ├─► Response Dispatched to Client │ ▼cleanupContext Execution ├─► Drain & execute all after() background callbacks ├─► Clear request-scoped DI cache (diCache.clear()) ├─► Purge request state keys (delete state[key]) └─► Unlink circular references (req = null, res = null, app = null)- Deterministic Teardown (
cleanupContext): When a response emits the'finish'or'close'event, ExisJS executes all queuedafter()callbacks, empties the request-scoped dependency injection cache, and clears the context state. - Circular Reference Scrubbing: Circular references between request, response, and app instances are set to
nullon cleanup to allow immediate V8 Garbage Collection. - Async Continuation Isolation: Context state is isolated per request branch, preventing state leakage across concurrent HTTP requests.