nest_04_providers - Providers & Dependency Injection¶
DI is not magic , it's just a factory pattern with a fancy name and runtime metadata But understanding it means the difference between gracefully adding features and fighting NestJS every time you create a new service that needs one more dependency
what's in here¶
- @Injectable decorator and provider registration
- Constructor injection
- Provider scopes: DEFAULT (singleton) , REQUEST , TRANSIENT
- Custom providers and factory functions
- Optional dependencies and circular dependency resolution
the @Injectable decorator¶
import { Injectable } from '@nestjs/common'
@Injectable()
export class UsersService {
private users: User[] = []
findAll(): User[] {
return this.users
}
findOne(id: string): User | undefined {
return this.users.find(u => u.id === id)
}
}
@Injectable() marks the class as a provider that the DI container can manage Without it , Nest won't instantiate this class or inject its dependencies Every service , repository , helper , and utility that needs DI gets this decorator
constructor injection¶
import { Injectable } from '@nestjs/common'
import { UsersRepository } from './users.repository'
import { EmailService } from '../email/email.service'
@Injectable()
export class UsersService {
constructor(
private readonly usersRepository: UsersRepository,
private readonly emailService: EmailService
) {}
}
Nest reads the constructor parameter types at runtime (thanks to TypeScript emitDecoratorMetadata) and resolves them from the DI container private readonly is shorthand - it declares and assigns the parameter in one line If you don't use private , you must assign it yourself:
export class UsersService {
private usersRepository: UsersRepository
constructor(usersRepository: UsersRepository) {
this.usersRepository = usersRepository
}
}
Use the shorthand. Everyone does
provider registration¶
Injection only works if the provider is registered in a module:
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
@Module({
controllers: [UsersController],
providers: [UsersService] // registered here
})
export class UsersModule {}
The providers array tells Nest which classes it can inject If you forget to register UsersService , Nest throws: Nest can't resolve dependencies of UsersController
provider scopes¶
import { Injectable, Scope } from '@nestjs/common'
// DEFAULT - singleton (one instance for whole app)
@Injectable() // same as @Injectable({ scope: Scope.DEFAULT })
export class DatabaseService {
constructor() {
console.log('DatabaseService initialized (once)')
}
}
// REQUEST - new instance per HTTP request
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
constructor(@Inject(REQUEST) private request: any) {
console.log('New instance per request')
}
}
// TRANSIENT - new instance per injection
@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService {
constructor() {
console.log('New instance every injection')
}
}
DEFAULT (singleton) - same instance shared everywhere , state is shared , memory efficient REQUEST - new instance for each HTTP request , can inject the raw request object , useful for per-request context TRANSIENT - new instance every time it's injected , each consumer gets their own copy , memory heavy
99% of your providers should be DEFAULT scope REQUEST scope adds complexity because it can't be injected into singleton-scoped providers TRANSIENT is rarely needed unless you're building something like a per-consumer logger or state tracker
custom providers with useFactory¶
Sometimes you need more control over how a provider is created:
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
@Module({
imports: [ConfigModule],
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (configService: ConfigService) => {
const connection = await createConnection({
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
user: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD')
})
return connection
},
inject: [ConfigService]
}
]
})
export class DatabaseModule {}
useFactory takes a function that returns the provider value inject lists the dependencies Nest should inject into the factory function The token 'DATABASE_CONNECTION' is used to inject this provider elsewhere:
@Injectable()
export class UsersRepository {
constructor(@Inject('DATABASE_CONNECTION') private connection: Connection) {}
}
Use string tokens when you're providing non-class values (connections , configs , third-party libraries)
optional dependencies¶
import { Injectable, Optional, Inject } from '@nestjs/common'
@Injectable()
export class AnalyticsService {
constructor(
@Optional() @Inject('SEGMENT_KEY') private segmentKey?: string
) {
if (!this.segmentKey) {
console.log('Segment not configured - analytics disabled')
}
}
}
@Optional() prevents Nest from throwing when the dependency isn't registered Useful for features that should degrade gracefully (logging , analytics , caching)
circular dependencies¶
Circular dependencies happen when Provider A depends on Provider B which depends on Provider A Nest detects these at startup and throws a clear error
import { Injectable, forwardRef, Inject } from '@nestjs/common'
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService
) {}
}
forwardRef() tells Nest to defer resolution - the class will be available at runtime even if it's not fully initialized yet Both sides of the circular dependency need forwardRef() on the injected parameter
// in AuthModule
@Module({
imports: [forwardRef(() => UsersModule)]
})
export class AuthModule {}
Circular deps aren't always bad but they're often a design smell Sometimes they mean you should extract the shared dependency into a third module
property injection (alternative)¶
@Injectable()
export class UsersService {
@Inject(CacheService)
private cacheService: CacheService
}
Property injection uses @Inject() on the property directly , no constructor needed Works but makes testing harder because you can't override the dependency in constructor args Stick to constructor injection unless you have a specific reason not to
prerequisites¶
nest_03_controllers - Controllers & Request Handling