Dependency injection is a design pattern in which a class declares the collaborators it needs, typically through its constructor, and an external container supplies configured instances rather than the class constructing them itself.
In simpler words
Instead of a controller writing new TicketsService(), it just asks for TicketsService in its constructor and Nest hands over the instance.
DI is the hardest Nest concept to fake your way through. Intermediate engineers must reason about tokens, module visibility, scope, and circular graphs — not only “put it in the constructor.”
Explain why constructor injection beats "new" inside a class
Name the DI token Nest uses for a class vs @InjectRepository(Ticket)
Diagnose "Nest can't resolve dependencies" as missing provider/import/export
Explain why singleton services must not store request-scoped state on this
Sketch how a unit test swaps TicketsService for a fake provider
Why not just "new" the service?
Definition
Manually constructing a collaborator with new couples a class to one concrete implementation and its entire construction logic, which a dependency-injection container avoids by supplying an already-configured instance.
In simpler words
new TicketsService() ties your controller to exactly one version of that service forever; asking for it lets Nest swap in whatever is configured — including a test double.
If TicketsController called new TicketsService() itself, it would also need to construct whatever TicketsService needs (a Repository<Ticket>), and so on down the chain — construction logic would leak into every consumer.
With DI, TicketsController only declares "I need a TicketsService" in its constructor signature. Nest resolves the whole chain — TicketsService, its Repository<Ticket>, that repository’s connection — once, using the module graph.
Intermediate rule: if you reach for new on a Nest-managed class, you have left the container. Prefer injection even for helpers that look “simple.”
Nest sees the TicketsService type token, finds it in TicketsModule.providers, constructs (or reuses) it, and passes it in.
Mistake: manual construction
// Wrong — bypasses the DI container
const ticketsService = new TicketsService(/* how do you get the repo? */);
// Right
constructor(private readonly ticketsService: TicketsService) {}
Manual construction skips Nest’s provider scoping and cannot be swapped for a test double.
Tokens: class vs custom vs repository
Definition
A DI token is the lookup key Nest uses to find a provider; for class providers the token is usually the class itself, while custom providers and TypeORM repositories use string/symbol tokens or decorator-generated tokens.
In simpler words
Nest does not inject “by TypeScript type magic alone” — it injects by token. Decorators like @InjectRepository create a specific token for that entity’s repository.
Class provider: { provide: TicketsService, useClass: TicketsService } is implied when you list TicketsService in providers. Constructor(private ticketsService: TicketsService) asks for that class token.
@InjectRepository(Ticket) injects the repository registered by TypeOrmModule.forFeature([Ticket]). Without forFeature, Nest throws at boot: Nest can't resolve dependencies of TicketsService.
Custom tokens (string/symbol + @Inject("TOKEN")) appear when you inject config objects, clients, or alternate implementations. Prefer clear names over mystery strings.
Repository token in TicketsService
@Injectable()
export class TicketsService {
constructor(
@InjectRepository(Ticket)
private readonly ticketsRepo: Repository<Ticket>,
) {}
}
// TicketsModule must register the token:
@Module({
imports: [TypeOrmModule.forFeature([Ticket])],
providers: [TicketsService],
controllers: [TicketsController],
})
export class TicketsModule {}
forFeature creates the repository provider; @InjectRepository asks for it. Both sides are required.
Mistake: inject without registering
// Wrong — AuthService wants UsersService but UsersModule did not export it
@Module({ providers: [UsersService] }) // no exports
export class UsersModule {}
@Module({ imports: [UsersModule], providers: [AuthService] })
export class AuthModule {} // boom at runtime/boot
// Right — export what other modules may inject
@Module({ providers: [UsersService], exports: [UsersService] })
export class UsersModule {}
Importing a module is not enough. Only exported providers cross module boundaries.
Providers, scope, and singletons
Definition
A provider is any value, factory, or class that Nest’s dependency-injection container can register and supply; by default each provider is instantiated once per application and shared as a singleton.
In simpler words
Mark a class @Injectable() and list it in a module’s providers — Nest builds one instance and reuses it everywhere that asks for it.
Default scope is SINGLETON: one TicketsService for the process. That is correct for almost all HTTP APIs.
REQUEST scope creates a new instance per HTTP request. It is useful for request-bound context, but it forces every consumer of that provider toward request scope too — a cascade that hurts performance and surprises beginners.
Because singletons are shared, never store “current user” or “current ticket id” on this.something. Pass them as method arguments from the controller (@CurrentUser(), params, DTOs).
Two overlapping requests would overwrite currentUserId on a singleton. Method params do not.
Circular dependencies and forwardRef
Definition
A circular dependency occurs when module A needs a provider from module B while module B needs a provider from module A, so Nest cannot finish constructing either side without an explicit break such as forwardRef or a shared third module.
In simpler words
If two features import each other, Nest may fail at boot or need forwardRef — treat that as a design smell, not a normal pattern.
First preference: extract the shared piece (for example UsersModule exporting UsersService) so AuthModule and TicketsModule both import UsersModule without importing each other.
forwardRef(() => OtherModule) / @Inject(forwardRef(() => OtherService)) can break cycles, but they hide coupling. Prefer redrawing the boundary.
Symptoms: Nest cannot resolve dependencies, undefined injected values, or modules that only work when registered in a lucky order.
Users own identity data; auth and tickets consume it. That direction matches the domain.
Testing with DI
Definition
Unit tests replace real providers with test doubles by overriding the same DI tokens Nest would use in production, so the class under test receives fakes through its normal constructor.
In simpler words
You do not stub private fields — you tell the testing module “when someone asks for TicketsService, give this fake.”
NestTestingModule.createTestingModule({ controllers: [TicketsController], providers: [{ provide: TicketsService, useValue: fake }] }) wires the same injection path as production.
Fake only the next collaborator. Do not mock TypeORM inside the controller test if the controller only talks to TicketsService.