Dependency Injection Questions
Dependency injection is the beating heart of NestJS, so interviewers probe it heavily: how the IoC container resolves a graph of dependencies, what injection scopes mean for performance, how to register non-class providers, and how to break circular references. The answers below favor precise mental models over hand-waving, with runnable NestJS 10/11 code you can reason about aloud. Get these right and you signal that you understand why Nest behaves the way it does, not just which decorator to copy.
How does the IoC container resolve dependencies?
When the application bootstraps, Nest builds a dependency graph. Each provider decorated with @Injectable() is registered against an injection token (the class itself by default). When a class declares a constructor parameter, Nest reads the parameter’s type metadata — emitted by TypeScript when emitDecoratorMetadata is enabled — looks up the matching token in the module’s provider registry, instantiates it (recursively resolving its dependencies first), caches the singleton, and injects it.
The key points to mention: resolution is constructor-based, dependencies are singletons by default and cached per application, and visibility is scoped to the module unless a provider is exported.
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
// Nest reads the type `Repository`, finds its provider, injects the instance.
constructor(private readonly repo: Repository) {}
findAll() {
return this.repo.all();
}
}
Tip: Interfaces vanish at runtime, so
constructor(private svc: MyInterface)cannot be resolved. Inject a class or use a string/symbol token with@Inject().
What are the injection scopes?
By default every provider is a singleton — one instance shared across the whole app. Nest also supports REQUEST and TRANSIENT scopes. A scoped provider “bubbles up”: any consumer of a request-scoped provider also becomes request-scoped, which can hurt performance, so reach for it deliberately.
| Scope | Lifetime | Use case |
|---|---|---|
DEFAULT (singleton) | One instance for app lifetime | Stateless services, repositories |
REQUEST | New instance per incoming request | Per-request state, multi-tenant context |
TRANSIENT | New instance per consumer that injects it | Lightweight, non-shared helpers |
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TenantContext {
readonly tenantId: string;
constructor(@Inject(REQUEST) request: Request) {
this.tenantId = (request.headers['x-tenant-id'] as string) ?? 'default';
}
}
How do custom providers work?
Sometimes the token-to-instance mapping isn’t a plain class. Nest offers four provider shapes. useClass swaps an implementation, useValue injects a constant (great for mocks), useFactory computes a value (optionally with injected dependencies), and useExisting aliases an existing token.
import { Module } from '@nestjs/common';
const CONFIG = 'CONFIG_OPTIONS';
@Module({
providers: [
{ provide: CONFIG, useValue: { retries: 3 } },
{
provide: 'HTTP_CLIENT',
useFactory: (cfg: { retries: number }) => new HttpClient(cfg.retries),
inject: [CONFIG], // injected into the factory args, in order
},
{ provide: Logger, useClass: ConsoleLogger },
],
exports: [CONFIG, 'HTTP_CLIENT'],
})
export class CoreModule {}
Consumers retrieve string/symbol tokens with @Inject:
@Injectable()
export class ReportService {
constructor(@Inject('HTTP_CLIENT') private readonly http: HttpClient) {}
}
| Provider | Shape | When |
|---|---|---|
useClass | Class constructor | Swap implementation by environment |
useValue | Literal/object | Constants, config, test doubles |
useFactory | Function (sync or async) | Computed/dynamic values |
useExisting | Token alias | Expose one provider under two tokens |
How do you handle circular dependencies?
A circular dependency occurs when module A needs B and B needs A, or two providers reference each other. Nest cannot resolve which to instantiate first. The fix is forwardRef(), which defers token resolution until both are registered. It is needed on both sides of the cycle.
import { Injectable, Inject, forwardRef } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => AuthService))
private readonly auth: AuthService,
) {}
}
For module-level cycles, wrap the import: @Module({ imports: [forwardRef(() => AuthModule)] }). The honest senior answer is that a circular dependency usually signals a design smell — extracting shared logic into a third provider is often cleaner than forwardRef.
When do you use ModuleRef?
ModuleRef is an escape hatch for retrieving providers imperatively when constructor injection won’t do — for example resolving a token computed at runtime, or grabbing a request-scoped instance from a singleton. Use get() for singletons and resolve() for scoped providers (it returns a Promise and accepts a context id).
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class TaskRunner {
constructor(private readonly moduleRef: ModuleRef) {}
async run(token: string) {
// strict:false searches the entire app, not just the host module
const handler = await this.moduleRef.resolve(token, undefined, {
strict: false,
});
return handler.execute();
}
}
Output:
Resolved handler for token "EmailJob" and executed it.
Best practices
- Inject by class token where possible; reserve string/symbol tokens for non-class providers and use
InjectionTokenconstants instead of magic strings. - Keep providers singleton by default — only escalate to
REQUESTscope when you genuinely need per-request state, and remember the scope bubbles to consumers. - Use
useValueto inject mocks in tests rather than monkey-patching, keeping the IoC container in charge. - Prefer extracting a shared provider over
forwardRef()to eliminate circular dependencies at the design level. - Always
exportproviders you intend other modules to consume; module visibility is the most common “cannot resolve dependencies” cause. - Use
ModuleRef.resolve()(notget()) for transient or request-scoped providers, and pass{ strict: false }only when crossing module boundaries deliberately.