What is dependency injection in NestJS?
Dependency injection means a class receives the objects it needs instead of creating them. In Nest you declare dependencies as constructor parameters, and the IoC container builds them and passes them in.
@Injectable() export class OrdersService { constructor( private readonly payments: PaymentsService, private readonly mailer: MailService, ) {} }Benefits interviewers expect: no
newscattered through the code, one shared instance per provider, and tests can swapPaymentsServicefor a fake by overriding the provider instead of patching imports.How does Nest know which instance to inject, and can you inject an interface?
Every provider is registered under a token, and Nest matches the constructor parameter to a token. For a class, the token is the class itself, read from the parameter's type through
design:paramtypesmetadata.An interface does not exist at runtime, so it cannot be a token. Use a string, a symbol or an abstract class, and name it with
@Inject():export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY'); export interface PaymentGateway { charge(cents: number): Promise<string>; } @Module({ providers: [{ provide: PAYMENT_GATEWAY, useClass: StripeGateway }] }) export class PaymentsModule {} @Injectable() export class CheckoutService { constructor(@Inject(PAYMENT_GATEWAY) private gateway: PaymentGateway) {} }An abstract class works as a token without
@Inject, because it does exist at runtime.What are
useValue,useClass,useFactoryanduseExisting?They are the four kinds of custom provider, each registering a token with a different recipe:
useValue: a ready object or constant (config, a mock in tests).useClass: which class to instantiate for the token (an implementation per environment).useFactory: a function whose return value (or resolved promise) is the provider;injectlists the factory's own dependencies.useExisting: an alias, so two tokens share one instance.
NestJSimport { Controller, Get, Inject, Injectable, Module } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; const CONFIG = Symbol('CONFIG'); abstract class Mailer { abstract send(to: string): string; } @Injectable() class ConsoleMailer extends Mailer { send(to: string) { return `logged a mail to ${to}`; } } // GET /signup @Controller('signup') class SignupController { constructor( @Inject(CONFIG) private config: { env: string; retries: number }, @Inject('START_TIME') private started: string, private mailer: Mailer, ) {} @Get() run() { return { config: this.config, started: this.started, mail: this.mailer.send('ada@example.com') }; } } @Module({ controllers: [SignupController], providers: [ { provide: 'ENV', useValue: 'staging' }, { provide: CONFIG, useFactory: (env: string) => ({ env, retries: env === 'production' ? 5 : 1 }), inject: ['ENV'] }, { provide: 'START_TIME', useFactory: async () => 'ready' }, { provide: Mailer, useClass: ConsoleMailer }, ], }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();The response shows each one:
ENVis a value,CONFIGa factory that read it,START_TIMEan async factory awaited before the app started, andMailerresolved toConsoleMailer.How do you create a provider that needs an async setup, like a database connection?
Use an async
useFactory: Nest awaits the promise before it creates anything that depends on the provider, and before the app accepts requests.{ provide: 'REDIS', useFactory: async (config: ConfigService) => { const client = createClient({ url: config.get('REDIS_URL') }); await client.connect(); return client; }, inject: [ConfigService], }Startup fails if the promise rejects, which is usually what you want: a service without its database should not report itself healthy. Pair it with
onModuleDestroyoronApplicationShutdownin a wrapper class to close the connection on shutdown.What are the provider scopes in NestJS?
There are three:
DEFAULT(a singleton shared by the whole app),REQUEST(a new instance for each incoming request) andTRANSIENT(a new instance for each consumer that injects it).NestJSimport { Controller, Get, Injectable, Module, Scope } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; let created = 0; let controllers = 0; @Injectable() class CounterService { count = 0; } @Injectable({ scope: Scope.REQUEST }) class RequestCounter { id = ++created; count = 0; } // GET /visits @Controller('visits') class VisitsController { private instance = ++controllers; constructor(private shared: CounterService, private perRequest: RequestCounter) {} @Get() hit() { return { singleton: ++this.shared.count, requestScoped: ++this.perRequest.count, requestInstance: this.perRequest.id, controllerInstance: this.instance, }; } } @Module({ controllers: [VisitsController], providers: [CounterService, RequestCounter] }) class AppModule {} async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();Send the request a few times:
singletonkeeps counting, whilerequestScopedstays at1because each request gets a freshRequestCounter.controllerInstancealso grows: a controller that injects a request-scoped provider becomes request scoped itself. UseREQUESTonly for per-request state such as the tenant or the current user; prefer the default.What does "request scope bubbles up the injection chain" mean, and what does it cost?
If a provider is request scoped, every provider and controller that depends on it, directly or indirectly, becomes request scoped too, because a singleton cannot hold a per-request object.
One request-scoped
TenantContextin a repository makes every service and controller above it request scoped. The cost is building that whole subtree on every request, which adds latency and garbage collection, and lifecycle hooks such asonModuleInitare not called on request-scoped classes.Alternatives: pass the value as a method argument, read it from
AsyncLocalStorage(thenestjs-clspackage wraps it), or use durable providers so the subtree is kept per tenant rather than per request.When would you use a transient provider?
When each consumer needs its own instance, typically because the instance stores something about its consumer. The classic example is a logger that remembers the class it logs for:
@Injectable({ scope: Scope.TRANSIENT }) export class ScopedLogger extends ConsoleLogger {} @Injectable() export class OrdersService { constructor(private logger: ScopedLogger) { this.logger.setContext(OrdersService.name); } }OrdersServiceandUsersServiceeach get their ownScopedLogger, so setting the context in one does not change the other. Unlike request scope, a transient provider injected into a singleton is created once for that singleton, at startup.How do you handle a circular dependency in NestJS?
Wrap both sides of the cycle in
forwardRef(), which lets Nest resolve a class that is not defined yet when the other is created:@Injectable() export class OrdersService { constructor( @Inject(forwardRef(() => PaymentsService)) private payments: PaymentsService, ) {} } @Injectable() export class PaymentsService { constructor( @Inject(forwardRef(() => OrdersService)) private orders: OrdersService, ) {} }For modules importing each other, use
imports: [forwardRef(() => PaymentsModule)]on both. Then say what the interviewer wants to hear: a cycle is a design smell. Move the shared logic into a third provider, or replace one direction with an event, and the cycle disappears.What does
@Optional()do?@Optional()lets a constructor parameter be missing: if no provider is registered for that token, Nest injectsundefinedinstead of failing at startup.@Injectable() export class NotificationsService { constructor(@Optional() @Inject('SLACK_WEBHOOK') private webhook?: string) {} notify(text: string) { if (!this.webhook) return; // feature off in this environment // post to Slack } }Use it for features that may be switched off per environment. Do not use it to hide a wiring mistake: a dependency that must exist should fail loudly at startup.
What is
ModuleRef, and when do you use it instead of constructor injection?ModuleRefgives you the container itself, so you can look up or create providers at runtime.@Injectable() export class ReportRunner { constructor(private moduleRef: ModuleRef) {} async run(kind: 'pdf' | 'csv') { const token = kind === 'pdf' ? PdfReport : CsvReport; const report = this.moduleRef.get(token, { strict: false }); return report.build(); } }get()returns a singleton.resolve()returns a scoped (request or transient) instance.create()builds a class that is not registered.
Use it for a strategy picked at runtime, or to break a cycle. Do not use it everywhere: constructor injection shows dependencies in the signature, and a lookup hides them.
NestJS Dependency Injection Interview Questions
Tokens, custom providers, useFactory, scopes, circular dependencies and forwardRef.
10 questions