Dependency Injection
Dependency Injection (DI) is the mechanism that makes NestJS components loosely coupled and testable. Instead of creating dependencies yourself, you declare what you need and NestJS provides them.
The Problem DI Solves
Without DI, you'd hardcode dependencies:
// Tightly coupled — can't test without a real database
class PostsService {
private db = new PrismaClient(); // hardcoded dependency
async findAll() {
return this.db.post.findMany();
}
}
With DI, you declare what you need:
// ✓ Loosely coupled — inject a mock in tests
@Injectable()
class PostsService {
constructor(private readonly prisma: PrismaService) {} // declared, not created
async findAll() {
return this.prisma.post.findMany();
}
}
How NestJS DI Works
- Mark a class as injectable with
@Injectable() - Declare it as a
providerin a module - Request it in a constructor — NestJS creates and injects it
// 1. Injectable service
@Injectable()
export class EmailService {
async send(to: string, subject: string, body: string): Promise<void> {
// email logic
}
}
// 2. Register in module
@Module({
providers: [EmailService, UsersService],
controllers: [UsersController],
})
export class UsersModule {}
// 3. Inject where needed
@Injectable()
export class UsersService {
constructor(
private readonly prisma: PrismaService,
private readonly email: EmailService, // auto-injected
) {}
async register(data: RegisterDto) {
const user = await this.prisma.user.create({ data });
await this.email.send(user.email, 'Welcome!', '...');
return user;
}
}
Provider Types
Value Provider
Inject a constant value:
@Module({
providers: [
{
provide: 'APP_CONFIG',
useValue: { maxUploadSize: 10 * 1024 * 1024 },
},
],
})
Inject it with @Inject:
constructor(@Inject('APP_CONFIG') private config: AppConfig) {}
Factory Provider
Compute the value at startup:
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: () => {
return new Redis({ host: process.env.REDIS_HOST });
},
},
],
})
Class Provider (default)
// These are equivalent
providers: [PostsService]
providers: [{ provide: PostsService, useClass: PostsService }]
// Swap implementation (great for testing/mocking)
providers: [{ provide: PostsService, useClass: MockPostsService }]
Scopes
By default, providers are singletons (one instance per app). You can change this:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestLogger {
// New instance created per HTTP request
// Has access to the current request context
}
| Scope | Behavior |
|---|---|
DEFAULT | Singleton — shared across entire app |
REQUEST | New instance per HTTP request |
TRANSIENT | New instance every time it's injected |
For most use cases, DEFAULT (singleton) is correct and most performant.
Testing with DI
DI makes unit testing straightforward — swap real implementations for mocks:
import { Test, TestingModule } from '@nestjs/testing';
import { PostsService } from './posts.service';
import { PrismaService } from '../prisma/prisma.service';
describe('PostsService', () => {
let service: PostsService;
const mockPrisma = {
post: {
findMany: vi.fn().mockResolvedValue([]),
findUnique: vi.fn(),
create: vi.fn(),
},
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PostsService,
{ provide: PrismaService, useValue: mockPrisma }, // inject mock
],
}).compile();
service = module.get<PostsService>(PostsService);
});
it('should return an array of posts', async () => {
const result = await service.findAll({ page: 1, limit: 10 });
expect(result.data).toEqual([]);
expect(mockPrisma.post.findMany).toHaveBeenCalled();
});
});
Circular Dependencies
Sometimes Module A needs Module B and Module B needs Module A. Use forwardRef to resolve:
// auth.module.ts
@Module({
imports: [forwardRef(() => UsersModule)],
})
// users.module.ts
@Module({
imports: [forwardRef(() => AuthModule)],
})
Circular dependencies are usually a sign of a design problem. Try to break the cycle by extracting a shared service into a third module.
Global Providers
Providers registered globally don't need to be imported in every module:
@Module({
providers: [AppConfigService],
exports: [AppConfigService],
})
@Global() // Available everywhere without importing
export class ConfigModule {}