Create Node.js request scopes
The Node adapter provides explicit async request scopes with
AsyncLocalStorage.
Use it when you want request-local DI in Node code outside the React Server Components render tree:
- Route Handlers;
- Server Actions with nested async code that should read request context without passing the container through every function;
- middleware-like server code;
- jobs and custom server entry points;
- code where passing the container through every function would be noisy.
For React Server Components in Next.js, keep using di-craft/next/server with
React’s cache primitive when you only need RSC render scope. Use this adapter
for Node runtime entry points when nested async code should read the current
request container without receiving it as an argument.
Runtime
Section titled “Runtime”This adapter is Node.js-only because it imports node:async_hooks. It is not
intended for Edge runtimes.
For the ALS path, prefer the Node runtime for Next.js entry points. Some Edge
runtimes polyfill node:async_hooks, but the semantics can differ from Node and
context may be lost across worker awaits. For Edge or other runtimes without
reliable AsyncLocalStorage, pass the container explicitly, use a callback
helper that owns the request lifecycle, or use an equivalent Edge-native context
primitive.
Imports
Section titled “Imports”import { createNodeDi } from "di-craft/node";Basic usage
Section titled “Basic usage”import { createToken, provideFactory, provideValue, Scopes } from "di-craft";import { createNodeDi } from "di-craft/node";
type RequestContext = { readonly requestId: string;};
class UsersService { private readonly requestContext: RequestContext;
constructor(requestContext: RequestContext) { this.requestContext = requestContext; }
list(): readonly string[] { return [`users:${this.requestContext.requestId}`]; }}
const REQUEST_CONTEXT = createToken<RequestContext>("REQUEST_CONTEXT");const USERS_SERVICE = createToken<UsersService>("USERS_SERVICE");
const { getRequestContainer, runWithRequestContainer } = createNodeDi({ providers: [ provideFactory(USERS_SERVICE, { scope: Scopes.Scoped, deps: { requestContext: REQUEST_CONTEXT }, useFactory: ({ requestContext }) => new UsersService(requestContext), }), ],});
const listUsers = async (): Promise<readonly string[]> => { await Promise.resolve();
return getRequestContainer().get(USERS_SERVICE).list();};
await runWithRequestContainer({ providers: [ provideValue(REQUEST_CONTEXT, { requestId: crypto.randomUUID(), }), ], run: async () => listUsers(),});getRequestContainer() works only inside runWithRequestContainer(). Calling it
outside an active async scope throws a clear error.
Await every async operation that reads the request container before the callback
returns. Async work started inside the scope can keep the AsyncLocalStorage
store, but runWithRequestContainer() disposes the request container as soon as
the callback settles.
Relationship With Next.js
Section titled “Relationship With Next.js”di-craft/node and di-craft/next/server solve different scope boundaries:
| Boundary | Adapter | Primitive |
|---|---|---|
| Server Components / nested RSC | di-craft/next/server |
React cache |
| Explicit Node async scopes | di-craft/node |
AsyncLocalStorage |
| Simple Route Handlers / Actions | Either explicit helper | Callback scope |
| Browser / Client Components | Neither server container | Serializable state |
AsyncLocalStorage does not magically make a Page/RSC render container equal to
a later Server Action container. It gives you a request container inside an
explicit async scope that you create with runWithRequestContainer().
For simple Next.js Route Handlers and Server Actions, runWithRequestContainer
from di-craft/next/server is usually enough. Reach for di-craft/node when
nested Node async calls need to read the current request container with
getRequestContainer().
For the full Next.js entry-point pattern, see Share request scope with AsyncLocalStorage.
Disposal
Section titled “Disposal”runWithRequestContainer() disposes the request container after the callback
settles, even when the callback throws:
await runWithRequestContainer({ run: async (container) => { container.get(DB_CONNECTION); },});Use onDispose on cached providers to release resources.
Do not start detached async work that later calls getRequestContainer() from
inside the callback. Pass the plain data it needs, or await that work before the
callback returns, so it does not read from a disposed request container.
Deferred work may inherit the runtime async context, but di-craft marks that
request scope inactive after disposal. Calling getRequestContainer() from the
deferred callback throws NodeRequestScopeError. Re-enter a new scope or pass
plain values to that work.
Testing
Section titled “Testing”The Node adapter composes with per-test request containers. In tests, wrap the
code under test with runWithRequestContainer() and register test-only
providers there instead of mocking modules:
await runWithRequestContainer({ providers: [provideValue(DB, fakeDb)], run: async () => { await serviceUnderTest(); },});Nested calls to getRequestContainer() resolve from that test scope, and the
container is disposed when the callback settles.