Skip to content
NestJS ns providers 5 min read

Dependency Injection Basics

Dependency injection is the backbone of every NestJS application. Instead of a class creating the objects it depends on, it simply declares what it needs and the framework supplies fully constructed instances at runtime. This inversion of responsibility keeps your code loosely coupled, trivially testable, and easy to reason about as the application grows. Understanding how the Nest IoC container builds and resolves the dependency graph is the single most valuable thing you can learn about the framework.

What is inversion of control?

Inversion of Control (IoC) is a design principle in which the flow of object creation is handed off to a container rather than written by hand inside your classes. In a traditional design, a service that needs a repository would call new UserRepository() itself, hard-wiring the concrete implementation and lifetime into the consumer. With IoC, the class declares the dependency and the container decides which implementation to provide and how long it should live.

Dependency injection is the concrete pattern Nest uses to implement IoC. The container owns instantiation, the consumer owns intent. The payoff is that you can swap implementations (a real mailer for a fake one in tests) without touching the consuming class.

The Nest IoC container

When your application boots, Nest creates a single application-wide IoC container. As it scans each module, it registers every class listed under providers into the container along with its metadata. The container becomes a registry of tokens mapped to provider definitions. By default the token is the class itself, so UsersService is registered under the token UsersService.

The container is responsible for three things: recording what can be injected, working out the order in which things must be created, and caching the resulting instances so the same singleton is shared everywhere.

Providers are only injectable where they are visible. A provider is available within its declaring module and to any module that imports the module that exports it. If a dependency cannot be found in scope, Nest throws a resolution error at startup, not at request time.

Constructor-based injection

The idiomatic way to receive a dependency in Nest is through the constructor. You mark the providing class with @Injectable() so the framework can read its metadata, then declare the dependency as a constructor parameter. Nest reads the parameter’s type via TypeScript’s emitted metadata and resolves the matching provider.

// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UsersRepository } from './users.repository';

@Injectable()
export class UsersService {
  constructor(private readonly usersRepository: UsersRepository) {}

  async findById(id: string) {
    return this.usersRepository.findOne(id);
  }
}
// users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.usersService.findById(id);
  }
}

Both providers are wired together in the module. Listing a class under providers registers it; the container handles the rest.

// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

@Module({
  controllers: [UsersController],
  providers: [UsersService, UsersRepository],
  exports: [UsersService],
})
export class UsersModule {}

The private readonly shorthand in the constructor is a TypeScript parameter property. It declares and assigns the field in one step, which is why you never write this.usersService = usersService yourself.

How Nest builds the dependency graph

At bootstrap Nest does not create providers in the order you list them. It builds a directed graph where each node is a provider and each edge points from a consumer to a dependency. It then resolves the graph bottom-up: a provider is only instantiated once all of its own dependencies already exist.

For the module above the graph looks like this:

UsersController ──▶ UsersService ──▶ UsersRepository

Resolution proceeds in reverse: UsersRepository is created first because it has no dependencies, then UsersService receives it, then UsersController receives the service. If two providers depend on each other directly Nest cannot order them and throws a circular dependency error.

You can watch the container resolve everything by enabling verbose logging.

NEST_DEBUG=true npm run start:dev

Output:

[InstanceLoader] UsersRepository dependencies initialized
[InstanceLoader] UsersService dependencies initialized
[InstanceLoader] UsersModule dependencies initialized
[RoutesResolver] UsersController {/users}
[RouterExplorer] Mapped {/users/:id, GET} route

How instances are resolved

The way the container hands back an instance depends on the provider’s scope. The table below summarizes the resolution behaviour you get by default and the alternatives.

ScopeWhen createdInstancesTypical use
DEFAULT (singleton)Once, at bootstrapOne shared across the appStateless services, repositories
REQUESTPer incoming requestOne per requestPer-request context, tenant data
TRANSIENTEvery injection pointFresh each timeLightweight, stateful helpers

For the default singleton scope, the first time a token is needed the container instantiates the class, caches the instance, and injects that same object into every consumer. Because the instance is cached, expensive setup happens exactly once.

If you need an implementation that is not a plain class — a value, a factory result, or an aliased token — you move beyond the shorthand and use the full provider object syntax, which is covered in the custom providers topics.

Best practices

  • Always prefer constructor injection over manually instantiating dependencies; let the container own object lifetimes.
  • Decorate every injectable class with @Injectable() so Nest can read and reflect its dependency metadata.
  • Keep providers stateless where possible so the default singleton scope is safe to share across the whole application.
  • Export a provider from its module only when another module genuinely needs it — minimal surface keeps the graph readable.
  • Depend on abstractions (interfaces backed by injection tokens) rather than concrete classes when you expect to swap implementations.
  • Avoid bidirectional dependencies between providers; refactor shared logic into a third provider before reaching for forwardRef.
Last updated June 14, 2026
Was this helpful?