Skip to content

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


next -> nest_05_modules - Modules & Module Patterns