Scope Route Handlers and Server Actions
Use runWithRequestContainer() where your code owns the full lifecycle. It
creates a fresh child container, passes it to the callback, and disposes it in a
finally block.
Route Handler
Section titled “Route Handler”Register request-specific values when entering the scope and resolve services inside the callback:
import { createToken, provideFactory, provideValue, Scopes } from "di-craft";import { createNextDi } from "di-craft/next/server";
type RequestCache = <T>(factory: () => T) => () => T;
// Route Handlers use `runWithRequestContainer`, so this example does not rely// on React request memoization. In a real Next app, pass `cache` from "react".const requestCache: RequestCache = (factory) => factory;
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 { runWithRequestContainer } = createNextDi({ cache: requestCache, providers: [ provideFactory(USERS_SERVICE, { scope: Scopes.Scoped, deps: { requestContext: REQUEST_CONTEXT }, useFactory: ({ requestContext }) => new UsersService(requestContext), }), ],});
export const GET = async (request: Request): Promise<Response> => { // This is a fresh explicit scope for the Route Handler. It is not the same // container created during a Page/RSC render. return runWithRequestContainer({ providers: [ provideValue(REQUEST_CONTEXT, { requestId: request.headers.get("x-request-id") ?? crypto.randomUUID(), }), ], run: async (container) => { const users = container.get(USERS_SERVICE).list();
return Response.json({ users }); }, });};
await GET(new Request("https://example.com/users"));Server Action
Section titled “Server Action”The same helper gives each Server Action its own explicit scope:
import { createToken, provideFactory, provideValue, Scopes } from "di-craft";import { createNextDi } from "di-craft/next/server";
type RequestCache = <T>(factory: () => T) => () => T;
// Server Actions use `runWithRequestContainer`, so this example does not rely// on React request memoization. In a real Next app, pass `cache` from "react".const requestCache: RequestCache = (factory) => factory;
type Actor = { readonly id: string;};
class AuditLog { private readonly actor: Actor;
constructor(actor: Actor) { this.actor = actor; }
record(action: string): string { return `${this.actor.id}:${action}`; }}
const ACTOR = createToken<Actor>("ACTOR");const AUDIT_LOG = createToken<AuditLog>("AUDIT_LOG");
const { runWithRequestContainer } = createNextDi({ cache: requestCache, providers: [ provideFactory(AUDIT_LOG, { scope: Scopes.Scoped, deps: { actor: ACTOR }, useFactory: ({ actor }) => new AuditLog(actor), }), ],});
export const saveUserAction = async (formData: FormData): Promise<string> => { "use server";
// This is a fresh explicit scope for the Server Action. It is not the same // container created during a Page/RSC render. return runWithRequestContainer({ providers: [ provideValue(ACTOR, { id: String(formData.get("actorId") ?? "anonymous"), }), ], run: async (container) => { return container.get(AUDIT_LOG).record("save-user"); }, });};
const formData = new FormData();formData.set("actorId", "user-1");
await saveUserAction(formData);runWithRequestContainer() does not bind the container to async context. Pass
the callback argument through your code, or use
AsyncLocalStorage when deep calls need to
read the current request container.