Execution context
The Nael container tracks every unit of work using a context ID. Request-scoped providers, interceptors, and guards all look up that ID so cross-cutting data (tenant IDs, trace info, auth state) stays isolated. This page explains how the context flows through HTTP and GraphQL platforms and how you can create custom scopes for background jobs or schedulers.
HTTP and GraphQL automatically create contexts
When you call createHttpApplication() or createGraphqlApplication(), the platform allocates a fresh context ID per
incoming request, resolves request-scoped providers under that ID, and disposes the context after the response. You can safely inject
Scope.REQUEST services anywhere in the call tree.
import { createHttpApplication } from '@nl-framework/http';import { Controller, Get, Injectable, Scope } from '@nl-framework/core';@Injectable({ scope: Scope.REQUEST })class RequestMetadata { readonly startedAt = Date.now(); url?: string;}@Controller('health')class HealthController { constructor(private readonly metadata: RequestMetadata) {} @Get() status() { return { ok: true, receivedAt: this.metadata.startedAt, url: this.metadata.url, }; }}const app = await createHttpApplication(AppModule);await app.listen();You can still mutate the scoped service inside middleware or guards (see below) and downstream controllers will observe the same instance.
Propagating data inside a request
Inject the request-scoped service inside middleware, guards, or interceptors via ctx.container.resolve(). Whatever you write to
that instance is visible later in controllers, resolvers, or services resolved from the same context ID.
import { MiddlewareHandler } from '@nl-framework/http';import { RequestContext } from '@nl-framework/http';export const tenantResolver: MiddlewareHandler = async (ctx, next) => { const tenantId = ctx.request.headers.get('x-tenant-id'); const tenant = await ctx.container.resolve(TenantContext); tenant.set(tenantId); return next();};Guards and interceptors can use the same pattern. Under the hood, the platform passes the active context ID into the container whenever it resolves a dependency.
Creating your own execution contexts
Outside HTTP/GraphQL—you might be in a scheduler, CLI command, or background worker—you can still get request-like scoping. Leverage the
ApplicationContext to create, use, and release context IDs manually.
import { Application, Scope, Injectable } from '@nl-framework/core';@Injectable({ scope: Scope.REQUEST })class JobState { id?: string;}const app = new Application();const context = await app.bootstrap(AppModule);const jobId = context.createContextId('nightly-report');try { const state = await context.get(JobState, { contextId: jobId }); state.id = 'job-42'; await runReport(state);} finally { context.releaseContext(jobId);}Always call releaseContext() in a finally block so the container can dispose transient instances and avoid memory
leaks.
ModuleRef respects the current context
If you need to resolve dependencies lazily (e.g., inside a method rather than the constructor), inject ModuleRef. Calls to
resolve() reuse the active context ID by default, so request-scoped providers remain consistent even when you defer their
creation.
import { Injectable, ModuleRef } from '@nl-framework/core';@Injectable()class ReportRunner { constructor(private readonly moduleRef: ModuleRef) {} async execute() { const mailer = await this.moduleRef.resolve(MailerService, { strict: false }); await mailer.send(); }}Best practices
- Use descriptive labels when calling
createContextId('job-42'); they show up in logs to aid debugging. - Avoid mixing context IDs. If you spawn async work from a request, pass the context ID explicitly so downstream services can reuse it.
- Request-scoped providers can inject singletons, but the reverse is not allowed—keep singleton services stateless or move them to request scope.