Injection scopes
Providers can live for the entire application, per incoming request, or be created every time they are injected. Choosing the right scope keeps expensive dependencies cached while still giving you access to per-request metadata when you need it.
Singleton (default)
Without extra options, every @Injectable() is a singleton. It is instantiated once during application bootstrap and shared with all
consumers. Use this for stateless services, caches, config readers, or clients that should be reused.
import { Injectable } from '@nl-framework/core';@Injectable()export class FeatureFlagService { private cache = new Map<string, boolean>(); isEnabled(flag: string) { return this.cache.get(flag) ?? false; }}Be careful not to store request-specific data in singletons. Prefer method arguments or scoped providers for that data.
Request scope
Set scope: Scope.REQUEST to create a new instance per HTTP or GraphQL request. The container ties the lifecycle to the current
execution context so you can access headers, user info, or trace IDs without manual plumbing.
import { Injectable, Scope } from '@nl-framework/core';import { RequestContext } from '@nl-framework/http';@Injectable({ scope: Scope.REQUEST })export class RequestLogger { constructor(private readonly ctx: RequestContext) {} log(message: string) { console.log('[' + this.ctx.request.url + ']', message); }}Request-scoped services can inject other request-scoped or singleton providers. Avoid injecting them into singletons unless you also mark the consuming provider as request-scoped, otherwise Nael will throw a circular scope error.
Transient scope
Transient providers are created every time they are injected. They never share state between injections, making them ideal for lightweight value objects, builders, or utilities that should produce a fresh instance per use.
import { Injectable, Scope, Inject } from '@nl-framework/core';import { randomUUID } from 'node:crypto';@Injectable({ scope: Scope.TRANSIENT })export class JobIdFactory { readonly id = randomUUID();}@Injectable()export class JobsService { constructor(@Inject(JobIdFactory) private readonly jobIdFactory: JobIdFactory) {} run() { return this.jobIdFactory.id; }}Because transient providers can fan out quickly, keep them cheap to create. They can inject singletons freely.
Scopes outside HTTP
Request scope also applies to GraphQL resolvers, scheduled jobs, and any custom context you establish via the core container. For advanced scenarios, you can create your own context IDs to isolate providers per unit of work.
import { Injectable, Scope } from '@nl-framework/core';import { GraphqlExecutionContext } from '@nl-framework/graphql';@Injectable({ scope: Scope.REQUEST })export class TenantContext { private tenantId?: string; setFromExecution(ctx: GraphqlExecutionContext) { this.tenantId = ctx.getContext().tenantId; } getId() { return this.tenantId; }}const app = await createHttpApplication(AppModule);const appContext = app.getApplicationContext();const contextId = appContext.createContextId('job-42');try { const tenant = await appContext.get(TenantContext, { contextId }); tenant.setFromExecution(jobContext); await jobRunner.execute(tenant);} finally { appContext.releaseContext(contextId);}To populate data, hook into the relevant execution context (HTTP guards, GraphQL interceptors, schedulers, etc.) and set values on the scoped service.