Skip to content

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.

Register request-specific values when entering the scope and resolve services inside the callback:

examples/next/route-handler.ts
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"));

The same helper gives each Server Action its own explicit scope:

examples/next/server-action.ts
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.