title: 'Execution context · Nael Framework' description: 'Understand how Nael propagates context IDs across HTTP, GraphQL, and custom workloads for scoped providers.'

Fundamentals

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.

Request metadata scoped per HTTP call
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.

Attach tenant data from middleware
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.

Manual context for background jobs
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.

Lazy resolution with ModuleRef
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.