Introduction
Dependency Injection (DI) is what makes NestJS applications testable, maintainable, and loosely coupled. Instead of classes creating their own dependencies, the framework injects them automatically. This is the foundation of enterprise-grade architecture.
Key Concepts
- Provider: Any class that can be injected as a dependency (services, repositories, factories)
- @Injectable(): Decorator that marks a class as available for DI
- Dependency Injection: Design pattern where dependencies are provided rather than created
- IoC Container: The Nest runtime that manages instance creation and injection
Real World Context
Without DI, you'd write:
typescriptclass UserController { private service = new UserService(new DatabaseConnection()); }
This is tightly coupled—testing requires a real database. With DI:
typescriptclass UserController { constructor(private userService: UserService) {} }
Now you can inject a mock UserService in tests. This pattern enables unit testing, swapping implementations, and managing complex dependency graphs.
Deep Dive
Creating a Provider
typescriptimport { Injectable } from '@nestjs/common'; @Injectable() export class CatsService { private readonly cats: Cat[] = []; create(cat: Cat): Cat { this.cats.push(cat); return cat; } findAll(): Cat[] { return this.cats; } findOne(id: string): Cat | undefined { return this.cats.find(cat => cat.id === id); } }
Injecting a Provider
Constructor injection is the standard approach:
typescript@Controller('cats') export class CatsController { constructor(private readonly catsService: CatsService) {} @Get() findAll() { return this.catsService.findAll(); } }
Registering Providers
Providers must be registered in a module:
typescript@Module({ controllers: [CatsController], providers: [CatsService], }) export class CatsModule {}
Provider Types
| Type | Use Case | Syntax |
|---|---|---|
| Standard | Most services | providers: [CatsService] |
| Value | Configuration, mocks | { provide: 'API_KEY', useValue: 'abc123' } |
| Class | Conditional implementations | { provide: Logger, useClass: ProdLogger } |
| Factory | Dynamic creation | { provide: 'DB', useFactory: () => ... } |
| Existing | Alias for another provider | { provide: 'ALIAS', useExisting: RealService } |
Aliasing with useExisting
Sometimes you need the same provider under a different token:
typescript@Module({ providers: [ ConnectionService, { provide: 'DATABASE_CONNECTION', useExisting: ConnectionService }, ], }) export class AppModule {}
Unlike useClass, which creates a new instance, useExisting returns the same singleton instance as the original provider. This is useful for backward compatibility or providing alternative injection tokens.
Injection Scopes
By default, providers are singletons (one instance per app). You can change this:
typescript@Injectable({ scope: Scope.REQUEST }) export class RequestScopedService {}
| Scope | Lifetime | Use Case |
|---|---|---|
| DEFAULT | Singleton | Most services |
| REQUEST | Per HTTP request | Request-specific data |
| TRANSIENT | New instance each injection | Stateful helpers |
Common Pitfalls
- Forgetting @Injectable(): The decorator is required for TypeScript to emit metadata. Without it, injection silently fails.
- Not registering in module: A provider must be in the
providersarray of a module to be injectable. - Circular dependencies: Service A injects B, B injects A. Use
forwardRef()to resolve.
Best Practices
- Single Responsibility: Each service handles one domain concern
- Interface-based design: Depend on abstractions, inject implementations
- Favor constructor injection: It's explicit and works with TypeScript types
- Keep providers stateless when possible: Easier to test and reason about
Summary
Providers are injectable classes marked with @Injectable(). The NestJS IoC container creates and injects them automatically via constructor parameters. This enables loose coupling, testability, and clean architecture.
Code Examples
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users: User[] = [];
create(dto: CreateUserDto): User {
const user = { id: Date.now(), ...dto };
this.users.push(user);
return user;
}
findOne(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
}