Chapter 33 — Dependency Injection in NestJS
📖 Definition
Dependency Injection (DI) is a design pattern in which a class declares the dependencies it needs (usually in the constructor) and the framework supplies them at runtime, instead of the class instantiating them directly.
🔍 Why DI?
| Without DI | With DI |
|---|---|
| Class creates its own deps → tight coupling | Class accepts deps → loose coupling |
| Hard to swap (e.g., mock in tests) | Easy to swap implementations |
| Singletons managed manually | Container manages lifetimes |
NestJS has a built-in IoC (Inversion of Control) container that:
- Reads constructor parameter types (via TypeScript metadata).
- Looks up matching providers in the module graph.
- Instantiates and injects them.
💻 Code Example — Provider + Injection
// user.repository.ts
@Injectable()
export class UserRepository {
findById(id: string) { /* … */ }
}
// user.service.ts
@Injectable()
export class UserService {
constructor(private readonly repo: UserRepository) {} // ← injected
get(id: string) {
return this.repo.findById(id);
}
}
// user.controller.ts
@Controller("users")
export class UserController {
constructor(private readonly service: UserService) {} // ← injected
@Get(":id")
get(@Param("id") id: string) {
return this.service.get(id);
}
}
// user.module.ts
@Module({
controllers: [UserController],
providers: [UserService, UserRepository],
})
export class UserModule {}Nest sees UserController needs UserService, which needs UserRepository. It instantiates them in order and injects.
💻 Code Example — Provider Types
// 1. Class provider (default)
{ provide: UserService, useClass: UserService }
// Shortcut: just UserService
// 2. Value provider
{ provide: "CONFIG", useValue: { apiKey: "abc" } }
// 3. Factory provider
{
provide: "DB_CONNECTION",
useFactory: async (config: ConfigService) => {
return await connectDb(config.get("DB_URL"));
},
inject: [ConfigService],
}
// 4. Existing (alias)
{ provide: "USER_SVC", useExisting: UserService }💻 Code Example — Injecting by Token
@Module({
providers: [
{ provide: "STRIPE_KEY", useValue: process.env.STRIPE_KEY },
],
exports: ["STRIPE_KEY"],
})
export class ConfigModule {}
@Injectable()
export class PaymentService {
constructor(@Inject("STRIPE_KEY") private readonly key: string) {}
}💻 Code Example — Scopes
@Injectable({ scope: Scope.DEFAULT }) // singleton — default
@Injectable({ scope: Scope.REQUEST }) // new instance per HTTP request
@Injectable({ scope: Scope.TRANSIENT }) // new instance per consumer
export class UserService { /* … */ }- DEFAULT — best performance; share across all requests.
- REQUEST — useful when you need per-request state (current user, tenant id).
- TRANSIENT — for utility classes that should never be shared.
💻 Code Example — Module Sharing
@Module({
providers: [DbConnection, UserRepository],
exports: [UserRepository], // ← visible to importers
})
export class DbModule {}
@Module({
imports: [DbModule], // ← brings UserRepository into scope
providers: [UserService],
})
export class UserModule {}If a provider isn't exports-ed, it's private to its module.
💻 Code Example — Global Module
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
// Now LoggerService is available everywhere without importing LoggerModule.Use sparingly — global modules can obscure dependencies.
💻 Code Example — Mocking in Tests
const module = await Test.createTestingModule({
controllers: [UserController],
providers: [
UserService,
{ provide: UserRepository, useValue: { findById: jest.fn().mockReturnValue({ id: "1", name: "Mock" }) } },
],
}).compile();
const ctrl = module.get<UserController>(UserController);
expect(await ctrl.get("1")).toEqual({ id: "1", name: "Mock" });💻 Code Example — Circular Dependencies
@Injectable()
export class A {
constructor(@Inject(forwardRef(() => B)) private readonly b: B) {}
}
@Injectable()
export class B {
constructor(@Inject(forwardRef(() => A)) private readonly a: A) {}
}⚠️ Circular deps are a smell. Prefer extracting shared logic into a third class.
🌍 Real-World Impact
- DI makes large codebases maintainable; one service can be reused across HTTP, GraphQL, queue workers, CLI.
- Tests become trivial — inject mocks.
- The Nest module graph mirrors your domain — each feature is one module.
🎯 Likely Interview Questions
- What is Dependency Injection?
- What's an
@Injectable()? - What are the provider types in NestJS?
- What are scopes?
- How do you handle circular dependencies? —
forwardRef, or restructure to break the cycle.