ExisJS

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>>(): T

Returns

TypeDescription
TThe 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

src/services/tenant.service.ts
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): void

Parameters

ParameterTypeDescription
keystringThe state key identifier.
valueanyThe value to store in the context.

Example

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

Returns

TypeDescription
RequestThe current request instance containing headers, params, query, body, ip, and contextual properties.

Example

src/loaders/user.loader.ts
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(): Response

Returns

TypeDescription
ResponseThe current response instance containing response headers, status codes, and streaming controls.

Example

src/services/auth.service.ts
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>): void

Parameters

ParameterTypeDescription
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

src/http/orders/route.ts
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(): App

Returns

TypeDescription
AppThe running application instance containing the DI container (app.container), logger (app.log), and configuration.

Example

src/services/audit.service.ts
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)
  1. Deterministic Teardown (cleanupContext): When a response emits the 'finish' or 'close' event, ExisJS executes all queued after() callbacks, empties the request-scoped dependency injection cache, and clears the context state.
  2. Circular Reference Scrubbing: Circular references between request, response, and app instances are set to null on cleanup to allow immediate V8 Garbage Collection.
  3. Async Continuation Isolation: Context state is isolated per request branch, preventing state leakage across concurrent HTTP requests.