dependency-injection
NestJS Dependency Injection
When to Use This Skill
Use this skill when:
- Working with custom providers beyond standard class providers
- Using factory providers for dynamic or async initialization
- Implementing value providers for configuration or constants
- Creating alias providers with useExisting
- Working with non-class-based injection tokens (strings or symbols)
- Implementing async providers that require initialization
- Understanding the three-step provider registration process
- Configuring provider scopes beyond singleton
What is Dependency Injection?
Dependency Injection (DI) is an inversion of control pattern where dependencies are provided to a class rather than the class creating them itself. NestJS has a built-in IoC (Inversion of Control) container that manages the entire DI system.
Three-Step Provider Registration
- Mark as Injectable - Use
@Injectable()decorator - Declare Dependencies - Specify in constructor
- Register in Module - Add to module's
providersarray
// Step 1: Mark as injectable
@Injectable()
export class CatsService {
// Step 2: Declare dependencies in constructor
constructor(private configService: ConfigService) {}
}
// Step 3: Register in module
@Module({
providers: [CatsService, ConfigService],
})
export class CatsModule {}
Standard Provider
The shorthand syntax:
@Module({
providers: [CatsService],
})
Is equivalent to:
@Module({
providers: [
{
provide: CatsService,
useClass: CatsService,
},
],
})
Key Points:
provide- The token used for injection (can be class, string, or symbol)useClass- The class to instantiate when the token is requested
Value Providers (useValue)
Inject a constant value, mock object, or external library:
const mockCatsService = {
findAll: () => ['cat1', 'cat2'],
create: (cat: Cat) => cat,
};
@Module({
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class CatsModule {}
Configuration Values
const configValues = {
port: 3000,
host: 'localhost',
database: {
url: 'mongodb://localhost/nest',
},
};
@Module({
providers: [
{
provide: 'CONFIG',
useValue: configValues,
},
],
})
export class AppModule {}
// Inject using @Inject()
@Injectable()
export class AppService {
constructor(@Inject('CONFIG') private config: any) {
console.log(config.port); // 3000
}
}
Use Cases:
- Testing (mocking dependencies)
- Injecting configuration objects
- External libraries that don't use DI
- Constants and static values
Non-Class-Based Provider Tokens
String Tokens
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}
// Injection requires @Inject()
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') private connection: Connection) {}
}
Symbol Tokens
More robust than strings, prevents naming collisions:
export const CONNECTION = Symbol('CONNECTION');
@Module({
providers: [
{
provide: CONNECTION,
useValue: connection,
},
],
})
export class DatabaseModule {}
// Inject using the symbol
@Injectable()
export class CatsRepository {
constructor(@Inject(CONNECTION) private connection: Connection) {}
}
Best Practice: Use symbols for token uniqueness
Class Providers (useClass)
Provide an alternative implementation based on environment or configuration:
abstract class ConfigService {
abstract get(key: string): any;
}
class DevelopmentConfigService extends ConfigService {
get(key: string) {
return devConfig[key];
}
}
class ProductionConfigService extends ConfigService {
get(key: string) {
return prodConfig[key];
}
}
@Module({
providers: [
{
provide: ConfigService,
useClass:
process.env.NODE_ENV === 'production'
? ProductionConfigService
: DevelopmentConfigService,
},
],
})
export class AppModule {}
Use Cases:
- Environment-specific implementations
- Strategy pattern implementations
- Feature flag-based class selection
- Testing with alternative implementations
Factory Providers (useFactory)
Create providers dynamically with custom logic:
@Module({
providers: [
{
provide: 'CONNECTION',
useFactory: (configService: ConfigService) => {
const options = configService.get('database');
return createConnection(options);
},
inject: [ConfigService],
},
],
})
export class DatabaseModule {}
Key Points:
useFactory- Function that returns the provider valueinject- Array of dependencies to inject into the factory- Factory function can be synchronous or asynchronous
Async Factory Providers
Factory providers can return Promises:
@Module({
providers: [
{
provide: 'ASYNC_CONNECTION',
useFactory: async (configService: ConfigService) => {
const config = configService.get('database');
const connection = await createConnection(config);
await connection.connect();
return connection;
},
inject: [ConfigService],
},
],
})
export class DatabaseModule {}
Factory with Multiple Dependencies
@Module({
providers: [
{
provide: 'REPOSITORY',
useFactory: (connection: Connection, logger: Logger) => {
logger.log('Creating repository');
return new Repository(connection);
},
inject: ['CONNECTION', Logger],
},
],
})
Optional Dependencies in Factories
@Module({
providers: [
{
provide: 'CACHE',
useFactory: (redis?: RedisClient) => {
if (redis) {
return new RedisCache(redis);
}
return new InMemoryCache();
},
inject: [{ token: 'REDIS_CLIENT', optional: true }],
},
],
})
Use Cases:
- Async initialization (database connections, API clients)
- Conditional provider creation
- Complex initialization logic
- Dynamic configuration
Alias Providers (useExisting)
Create an alias for an existing provider (shares the same instance):
@Module({
providers: [
CatsService,
{
provide: 'AliasedCatsService',
useExisting: CatsService,
},
],
})
export class CatsModule {}
Difference from useClass:
useExisting- Returns the same instance (alias)useClass- Creates a new instance
Interface-Based Injection
abstract class LoggerService {
abstract log(message: string): void;
}
@Injectable()
class ConsoleLogger implements LoggerService {
log(message: string) {
console.log(message);
}
}
@Module({
providers: [
ConsoleLogger,
{
provide: LoggerService,
useExisting: ConsoleLogger,
},
],
})
export class LoggerModule {}
// Inject using abstract class
@Injectable()
export class CatsService {
constructor(private logger: LoggerService) {}
}
Use Cases:
- Creating multiple tokens for the same provider
- Interface-based dependency injection
- Backward compatibility when refactoring
Exporting Custom Providers
Custom providers can be exported using their token:
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (configService: ConfigService) => {
return createConnection(configService.get('database'));
},
inject: [ConfigService],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'], // Export by token
})
export class DatabaseModule {}
Export the entire provider object:
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
export class DatabaseModule {}
Complete Example: Database Connection
// database.providers.ts
import { ConfigService } from '@nestjs/config';
import { Connection, createConnection } from './connection';
export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');
export const databaseProviders = [
{
provide: DATABASE_CONNECTION,
useFactory: async (configService: ConfigService): Promise<Connection> => {
const connection = await createConnection({
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
});
await connection.initialize();
return connection;
},
inject: [ConfigService],
},
];
// database.module.ts
import { Module } from '@nestjs/common';
import { databaseProviders, DATABASE_CONNECTION } from './database.providers';
@Module({
providers: [...databaseProviders],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule {}
// cats.repository.ts
import { Injectable, Inject } from '@nestjs/common';
import { DATABASE_CONNECTION } from './database.providers';
import { Connection } from './connection';
@Injectable()
export class CatsRepository {
constructor(
@Inject(DATABASE_CONNECTION)
private connection: Connection,
) {}
async findAll(): Promise<Cat[]> {
return this.connection.query('SELECT * FROM cats');
}
}
// cats.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database.module';
import { CatsRepository } from './cats.repository';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
@Module({
imports: [DatabaseModule],
controllers: [CatsController],
providers: [CatsService, CatsRepository],
})
export class CatsModule {}
Provider Scope and Injection
Injection Tokens Summary
| Token Type | Example | Injection Syntax |
|---|---|---|
| Class | CatsService |
constructor(private catsService: CatsService) |
| String | 'CONNECTION' |
constructor(@Inject('CONNECTION') private connection) |
| Symbol | CONNECTION |
constructor(@Inject(CONNECTION) private connection) |
When to Use @Inject()
Required:
- Non-class tokens (strings, symbols)
- Circular dependencies with
forwardRef() - Optional dependencies with
@Optional()
Not Required:
- Class-based tokens (TypeScript handles this)
Multi-Provider Pattern
Register multiple providers under the same token:
const loggerProviders = [
{
provide: 'LOGGER',
useClass: ConsoleLogger,
multi: true,
},
{
provide: 'LOGGER',
useClass: FileLogger,
multi: true,
},
];
@Module({
providers: loggerProviders,
})
export class LoggerModule {}
// Inject all providers as an array
@Injectable()
export class AppService {
constructor(@Inject('LOGGER') private loggers: Logger[]) {
// loggers is an array of [ConsoleLogger, FileLogger]
}
}
Dynamic Providers
Create providers at runtime:
function createDatabaseProviders(): Provider[] {
const providers: Provider[] = [];
for (const tenant of tenants) {
providers.push({
provide: `${tenant.name}_CONNECTION`,
useFactory: async () => {
return createConnection(tenant.config);
},
});
}
return providers;
}
@Module({
providers: [...createDatabaseProviders()],
})
export class MultiTenantModule {}
Best Practices
- Use symbols for custom tokens - Prevents naming collisions
- Prefer constructor injection - More explicit than property injection
- Use factories for async initialization - Database connections, API clients
- Export only what's needed - Minimize coupling between modules
- Use abstract classes for interfaces - Better than TypeScript interfaces for DI
- Document your providers - Especially custom tokens and factories
- Use useExisting for aliases - When you need multiple names for same instance
- Keep factories simple - Move complex logic to separate services
- Use proper typing - Type your factory return values and injected dependencies
- Organize providers - Group related providers in separate files
Testing Custom Providers
describe('CatsService', () => {
let service: CatsService;
let connection: Connection;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CatsService,
{
provide: 'CONNECTION',
useValue: {
query: jest.fn(),
insert: jest.fn(),
},
},
],
}).compile();
service = module.get<CatsService>(CatsService);
connection = module.get('CONNECTION');
});
it('should query the database', async () => {
await service.findAll();
expect(connection.query).toHaveBeenCalled();
});
});
Common Patterns
Repository Factory Pattern
export function createRepositoryProvider<T>(
entity: Type<T>,
): Provider {
return {
provide: `${entity.name}Repository`,
useFactory: (connection: Connection) => {
return connection.getRepository(entity);
},
inject: ['CONNECTION'],
};
}
@Module({
providers: [
createRepositoryProvider(Cat),
createRepositoryProvider(Dog),
],
})
export class RepositoriesModule {}
Environment-Based Configuration
const configProvider = {
provide: 'CONFIG',
useFactory: () => {
const env = process.env.NODE_ENV;
return env === 'production' ? productionConfig : developmentConfig;
},
};
@Module({
providers: [configProvider],
exports: ['CONFIG'],
})
export class ConfigModule {}
Lazy Initialization
const lazyServiceProvider = {
provide: 'LAZY_SERVICE',
useFactory: () => {
let service: HeavyService | null = null;
return {
get: () => {
if (!service) {
service = new HeavyService();
}
return service;
},
};
},
};