NestJS Dependency Injection

Dependency Injection (DI) is the system NestJS uses to provide classes with the other classes they need — automatically. Instead of creating objects manually inside your code, you declare what you need and NestJS supplies it. This one mechanism removes a huge amount of boilerplate and makes your code much easier to test.

The Problem Without Dependency Injection

Imagine you run a bakery. The baker needs an oven. Without DI, the baker would build their own oven every morning from scratch. That is wasteful, inconsistent, and completely unnecessary when the bakery already has ovens available.

In code terms, this looks like:

// Without DI — bad approach
class UsersController {
  private usersService: UsersService;

  constructor() {
    this.usersService = new UsersService(); // Baker builds their own oven
  }
}

This approach creates a new service instance every time, making it impossible to share state, swap implementations, or mock the service in tests.

The Solution: Dependency Injection

With DI, the bakery (NestJS container) owns the ovens. Bakers request an oven and receive one — pre-built, warmed up, and ready to use.

// With DI — correct approach
class UsersController {
  constructor(private readonly usersService: UsersService) {}
  // NestJS supplies the UsersService automatically
}

NestJS reads the constructor parameter types using TypeScript's metadata system and injects the correct instance. You declare the dependency; NestJS fulfills it.

How NestJS DI Works Step by Step

Step 1: UsersService is decorated with @Injectable()
         ↓
Step 2: UsersModule registers UsersService in providers[]
         ↓
Step 3: UsersController declares UsersService in its constructor
         ↓
Step 4: NestJS reads the constructor type (UsersService)
         ↓
Step 5: NestJS checks its container — finds UsersService
         ↓
Step 6: NestJS creates (or reuses) one UsersService instance
         ↓
Step 7: NestJS passes the instance into UsersController

This entire process happens automatically when the application starts. You write zero object-creation code.

The DI Container

NestJS maintains an internal container — a registry of all providers in the application. When a class needs a provider, the container looks it up and injects it. The container also manages provider lifecycles: creating instances, reusing singletons, and cleaning up when the application shuts down.

You can think of the container as a warehouse. Providers (services, repositories, helpers) sit on shelves. When a controller or service needs something, the warehouse finds it and delivers it instantly.

Constructor Injection

Constructor injection is the most common and recommended form of DI in NestJS. You declare dependencies in the constructor, and NestJS supplies them:

@Injectable()
export class OrdersService {
  constructor(
    private readonly usersService: UsersService,
    private readonly productsService: ProductsService,
    private readonly emailService: EmailService,
  ) {}

  async placeOrder(userId: number, productId: number) {
    const user = await this.usersService.findOne(userId);
    const product = await this.productsService.findOne(productId);
    await this.emailService.send(user.email, `Order confirmed: ${product.name}`);
  }
}

Three services inject into one service. NestJS resolves all three automatically. No manual wiring required.

Property Injection

NestJS also supports property injection using the @Inject() decorator. Use this when the class cannot use constructor injection (rare scenarios like abstract base classes):

@Injectable()
export class ReportsService {
  @Inject(UsersService)
  private usersService: UsersService;
}

Constructor injection is preferred in most cases because it makes dependencies visible and explicit. Property injection hides dependencies inside the class body.

Why DI Makes Testing Easier

DI enables you to swap real implementations with fake ones during testing. Instead of calling a real database, you inject a mock service that returns test data:

// In a test file
const moduleRef = await Test.createTestingModule({
  providers: [
    UsersController,
    {
      provide: UsersService,
      useValue: {
        findAll: () => [{ id: 1, name: 'Test User' }],
      },
    },
  ],
}).compile();

The controller still calls this.usersService.findAll() — but now it receives fake data. The real database never gets involved. Tests run instantly without any external connections.

Circular Dependencies

A circular dependency occurs when Module A needs Module B, and Module B needs Module A. NestJS detects this and throws an error at startup.

ServiceA → needs → ServiceB
ServiceB → needs → ServiceA  ← circular!

The fix is to use NestJS's forwardRef() utility, which delays the reference resolution:

constructor(
  @Inject(forwardRef(() => ServiceB))
  private serviceB: ServiceB,
) {}

A better long-term solution is to restructure your code so the circular dependency does not exist — usually by extracting the shared logic into a third service.

The Core Benefit

Dependency Injection removes the burden of managing object creation from your business code. Controllers and services focus entirely on their own logic. NestJS handles the wiring. The result is cleaner code, easier tests, and a codebase where replacing one implementation with another takes a single line change rather than a search-and-replace across the entire project.

Leave a Comment

Your email address will not be published. Required fields are marked *