Request Context
requestContext shares small values such as request ID, locale, and an authentication snapshot through a single request's call chain. By default, a VextJS adapter creates an independent AsyncLocalStorage scope for each incoming request. Middleware, handlers, and services called in that scope can read its data.
This association follows an async call chain, not a class or module. A service constructor or plugin setup normally has no request context. Business authorization, database filtering, and cross-process transport are separate responsibilities.
Run a concurrent example first
Prerequisite: the TypeScript API-only project from “Manual setup” in Quick Start, retaining its package.json, tsconfig.json, and startup scripts. Merge this configuration and add three files. No database, external service, or auth plugin is needed.
demoLabel is ordinary input used to observe isolation; it does not establish identity or permission. Run npm run dev; dev startup generates service type mappings. In another terminal, run this complete Node command from the project root:
Both responses should be 200. The first body.data has before and after equal to req-a, locale zh-CN, label a, forwarded tag tag-a, and authenticated: false; the second has req-b, en-US, b, tag-b, and false. Each response header and server JSON log should carry its corresponding request ID.
Without x-request-id, Vext generates an ID; without the language header, it uses the configured default. Stop dev, run npm run build and npm start, and repeat the requests. Stop the server with Ctrl+C afterward.
This example uses pretty: false to inspect full JSON. Default pretty output hides the displayed requestId; absence from the terminal view alone does not prove lost context.
Core concepts
What is AsyncLocalStorage?
Node.js is a single-threaded event loop but handles multiple concurrent requests at the same time. The traditional global variable method (such as global.currentRequestId) will be overwritten by later requests, causing race conditions.
AsyncLocalStorage maintains independent storage space for each asynchronous execution context, which can safely isolate data even in concurrent scenarios:
Life cycle
A response ending does not immediately clear its store; collection depends on the lifetime of related async resources and references. Do not call global requestContext.disable() per request; that can affect other requests. See the Node.js AsyncLocalStorage docs.
requestContext.enabled: false skips the framework-created HTTP scope. requestId.enabled: false only disables ID generation and its response header; an otherwise enabled context still contains locale and configured inbound header snapshots. Manual run() remains available, and code called inside a manual outer scope may still see that outer store.
Basic usage
Read requestId
To explicitly correlate business records in the request chain, read the current store. Do not cache a store once at module initialization and reuse it across requests:
Default app.logger and app.fetch usually read the ID automatically. Read it yourself when handing it to a business system. An inbound request ID is a correlation identifier; it is not guaranteed globally unique and is not a user identity.
Read locale
Independent request metadata middleware chooses locale from Accept-Language, locale.supported, and locale.default, regardless of whether request ID generation is enabled. With no match, it uses the configured default, which is en-US by default.
Default app.throw() uses the current app's language catalog and an applicable request locale; it does not borrow a locale from a store owned by another app. A manually created store does not run HTTP metadata or authentication middleware. See i18n and Error Handling.
RequestContextStore type
These are the public fields; import RequestContextStore from vextjs when using it. All fields are optional because a manually created store may contain only some values.
getStore() returns the same mutable store in this call chain; it does not copy or freeze it. Mutating store.auth is not authenticating a request or replacing route guards. Use req.auth for full authentication state; see Security.
Advanced usage
Write custom data in middleware
The route above writes demoLabel. To share that logic among routes, move it into middleware using the type extension already defined:
A file's existence does not execute it. Following Middleware, declare context-label in config.middlewares, then reference it in routes that need it. For global execution, import and mount it with app.use() in a plugin. After enabling it, remove the duplicate write in the earlier route. A business service reads the store in each method call rather than caching it on a singleton instance.
Extend the Store type
Add fields to the earlier src/types/request-context.d.ts. Keep import "vextjs" at the top so the declaration augments the existing module; ensure tsconfig includes the file:
Then requestContext.getStore()?.tenantId has type string | undefined without as any. Read authentication from store.auth rather than creating another source of truth for user ID or roles.
Multi-tenant data isolation
The context may carry a tenant ID whose ownership has already been verified. It does not authorize a request or automatically rewrite database queries. Copying x-tenant-id directly to the store and then using it as a database filter lets the caller choose any tenant; that is not isolation.
The business flow is:
- Identify the user through authentication middleware.
- Check the user's right to the selected tenant; reject on failure.
- Write the verified tenant ID to the current store.
- Explicitly use that tenant for every read, update, deletion, and insertion; reject if it is missing.
This helper belongs in src/utils, where the service loader will not mistake it for a service class. It only supplies a query condition; it does not perform steps 1 or 2:
The final tenant ID overwrites a same-named caller filter. See Database for actual integration and CRUD. Cover every business query path; this helper is not an automatic isolation plugin.
Performance tracking
This middleware depends on the startTime type extension above and must be mounted explicitly. It measures await next() including downstream middleware and handler; it does not mean all network bytes have reached the client.
finally also records downstream failures; the explicit undefined check retains a zero-valued start time. See the Access Log API and Hooks for logging and streaming lifecycle.
Keep context across async work
Native Promises, setTimeout, and setImmediate created inside the request chain usually retain its context; the service in the first example checks after one async wait. Here is an independent language-mechanism example:
Workers, processes, and queue consumers do not inherit an inbound HTTP store. Pass selected data explicitly and establish a new scope at the consumer. A timer created during a request can still read the old store after the response, so “scheduled work” alone does not prove the context is absent.
Custom thenables, callback libraries, or events triggered on a different chain may lose or change context. Inspect getStore() on both sides of that boundary. Use native Promises or AsyncResource as described in Node's context-loss guide when needed.
Create a context manually
This function can be called from an existing task entry; it does not create an auto-running task. It requires an initialized app and no extra business service:
run() returns the callback result, so await its Promise for an async callback. Manual run creates a store scope only; it does not run HTTP middleware, add auth, capture inbound headers, or schedule a task. Jobs covers discovery and scheduled execution.
Relationship with the built-in functions of the framework
The default access log also gets request ID through logger context. If only one service loses the ID, inspect the async chain that calls it.
requestContext API
requestContext.getStore()
Returns the RequestContextStore reference for the current scope, or undefined if none exists. A store may also come from a manual run, so its existence does not imply an HTTP request is being processed.
requestContext.run(store, callback)
Runs a callback under the supplied store and returns its result. Nested runs do not merge store fields; after the inner scope, the outer scope is restored. The caller handles exceptions or Promise rejection from the callback.
The adapter normally establishes HTTP scopes. For manual runs, create a separate store object each time; do not reuse a global mutable store.
Relationship with distributed tracing (traceId)
requestId vs traceId
A request ID correlates logs and service requests; it may come from an inbound header or be generated by Vext. A tracing SDK normally supplies traceId/spanId with real span lifecycles. Storing those fields in Vext does not create, sample, or export spans.
Mode 1: requestId as a correlation ID
If a shared correlation ID is sufficient, change the request ID header to x-trace-id. Merge this into existing configuration; omitting generate keeps the framework UUID generator:
The default logger field remains requestId; app.fetch uses the x-trace-id header. Renaming it does not generate W3C traceparent or an APM span, and does not ensure the ID satisfies an external tracing format.
Mode 2: requestId alongside an APM traceId
For APM, initialize and configure inbound/outbound instrumentation and exports for the chosen tracing SDK first, then associate current-span fields with logs. Passing ordinary headers only passes values; it does not create parent-child spans.
This bridge writes values returned by an already configured SDK into context. The integration supplies readActiveSpan:
Call it where both an HTTP context and target span are active. The default logger reads these fields, though a custom logger mixin can override trace_id and span_id; the built-in requestId rule differs. Update or clear fields when the span changes; one copy does not track the SDK afterward. See the OpenTelemetry example.
How propagateHeaders works
Merge this into the existing fetch configuration:
- Request metadata middleware captures allowlisted inbound headers in
store.propagatedHeaders. app.fetchreads that snapshot while building an outbound request.- It fills same-named headers that were not explicitly set; explicit outbound values win.
- The downstream tracing integration decides how to create spans. Copying an inbound traceparent does not create this service's outbound span.
Currently, per-call propagateRequestId: false only stops automatic ID injection; other captured headers still propagate. Per-call propagateHeaders: [] is not a switch that clears the captured snapshot. If the ID header is itself in the global capture list, it may still leave through the snapshot. Choose the global capture list according to outbound destinations; when a separate request must inherit nothing, use native fetch with explicit headers.
This fragment requires an existing app and known URL supplied by the caller:
Ordinary app.fetch header propagation and proxy forwarding are distinct entry points; see the HTTP client guide for proxy behavior.
Best Practices
1. Prefer built-in behavior
Default logger and fetch cover common ID correlation. For failures, inspect adapter scope, metadata writes, business reads/writes, and consumers in order instead of adding another global ID.
2. Store only request-scoped data
Keep small IDs, locale, and verified business identifiers. Avoid complete requests, large query results, or long-lived connections. Async resources that run after the response can extend the lifetime of referenced objects.
3. Handle undefined stores
Use optional chaining and explicit defaults for optional observability data. Fail when essential business context is missing; do not substitute a default tenant to bypass isolation. Startup, standalone tasks, and callbacks that lose context can lack a store.
4. Extend the Store with types
Use the module augmentation above and import requestContext at the call site. A type declaration does not make the framework write a new field automatically.
5. Avoid mutable shared objects in the store
Separate stores can still reference the same object. { ...shared } copies only one level; nested objects remain shared. Create independent values or use immutable data as needed. Do not cache a store or request data on a singleton service instance.
Troubleshooting
Next step
- Mount context-writing logic with Middleware.
- See Logger, HTTP Client, and i18n for consumers.
- Use Security to establish authentication and authorization before carrying business identity.
- For background work across requests, read Jobs; for tracing, see the OpenTelemetry example.