Chapter 65 โ€” Dependency Injection in NestJS

๐Ÿ“– Definition

Dependency Injection (DI) is a design pattern in which a class declares the dependencies it needs (usually in the constructor) and the framework supplies them at runtime, instead of the class instantiating them directly.

๐Ÿ” Why DI?

Without DI With DI
Class creates its own deps โ†’ tight coupling Class accepts deps โ†’ loose coupling
Hard to swap (e.g., mock in tests) Easy to swap implementations
Singletons managed manually Container manages lifetimes

NestJS has a built-in IoC (Inversion of Control) container that:

  1. Reads constructor parameter types (via TypeScript metadata).
  2. Looks up matching providers in the module graph.
  3. Instantiates and injects them.

๐Ÿ’ป Code Example โ€” Provider + Injection

// user.repository.ts
@Injectable()
export class UserRepository {
  findById(id: string) { /* โ€ฆ */ }
}

// user.service.ts
@Injectable()
export class UserService {
  constructor(private readonly repo: UserRepository) {}    // โ† injected

  get(id: string) {
    return this.repo.findById(id);
  }
}

// user.controller.ts
@Controller("users")
export class UserController {
  constructor(private readonly service: UserService) {}    // โ† injected

  @Get(":id")
  get(@Param("id") id: string) {
    return this.service.get(id);
  }
}

// user.module.ts
@Module({
  controllers: [UserController],
  providers: [UserService, UserRepository],
})
export class UserModule {}

Nest sees UserController needs UserService, which needs UserRepository. It instantiates them in order and injects.

๐Ÿ’ป Code Example โ€” Provider Types

// 1. Class provider (default)
{ provide: UserService, useClass: UserService }
// Shortcut: just UserService

// 2. Value provider
{ provide: "CONFIG", useValue: { apiKey: "abc" } }

// 3. Factory provider
{
  provide: "DB_CONNECTION",
  useFactory: async (config: ConfigService) => {
    return await connectDb(config.get("DB_URL"));
  },
  inject: [ConfigService],
}

// 4. Existing (alias)
{ provide: "USER_SVC", useExisting: UserService }

๐Ÿ’ป Code Example โ€” Injecting by Token

@Module({
  providers: [
    { provide: "STRIPE_KEY", useValue: process.env.STRIPE_KEY },
  ],
  exports: ["STRIPE_KEY"],
})
export class ConfigModule {}

@Injectable()
export class PaymentService {
  constructor(@Inject("STRIPE_KEY") private readonly key: string) {}
}

๐Ÿ’ป Code Example โ€” Scopes

@Injectable({ scope: Scope.DEFAULT })   // singleton โ€” default
@Injectable({ scope: Scope.REQUEST })   // new instance per HTTP request
@Injectable({ scope: Scope.TRANSIENT }) // new instance per consumer
export class UserService { /* โ€ฆ */ }
  • DEFAULT โ€” best performance; share across all requests.
  • REQUEST โ€” useful when you need per-request state (current user, tenant id).
  • TRANSIENT โ€” for utility classes that should never be shared.

๐Ÿ’ป Code Example โ€” Module Sharing

@Module({
  providers: [DbConnection, UserRepository],
  exports:   [UserRepository],            // โ† visible to importers
})
export class DbModule {}

@Module({
  imports:   [DbModule],                  // โ† brings UserRepository into scope
  providers: [UserService],
})
export class UserModule {}

If a provider isn't exports-ed, it's private to its module.

๐Ÿ’ป Code Example โ€” Global Module

@Global()
@Module({
  providers: [LoggerService],
  exports:   [LoggerService],
})
export class LoggerModule {}

// Now LoggerService is available everywhere without importing LoggerModule.

Use sparingly โ€” global modules can obscure dependencies.

๐Ÿ’ป Code Example โ€” Mocking in Tests

const module = await Test.createTestingModule({
  controllers: [UserController],
  providers: [
    UserService,
    { provide: UserRepository, useValue: { findById: jest.fn().mockReturnValue({ id: "1", name: "Mock" }) } },
  ],
}).compile();

const ctrl = module.get<UserController>(UserController);
expect(await ctrl.get("1")).toEqual({ id: "1", name: "Mock" });

๐Ÿ’ป Code Example โ€” Circular Dependencies

@Injectable()
export class A {
  constructor(@Inject(forwardRef(() => B)) private readonly b: B) {}
}

@Injectable()
export class B {
  constructor(@Inject(forwardRef(() => A)) private readonly a: A) {}
}

โš ๏ธ Circular deps are a smell. Prefer extracting shared logic into a third class.

๐ŸŒ Real-World Impact

  • DI makes large codebases maintainable; one service can be reused across HTTP, GraphQL, queue workers, CLI.
  • Tests become trivial โ€” inject mocks.
  • The Nest module graph mirrors your domain โ€” each feature is one module.

๐ŸŽฏ Likely Interview Questions

  1. What is Dependency Injection?
  2. What's an @Injectable()?
  3. What are the provider types in NestJS?
  4. What are scopes?
  5. How do you handle circular dependencies? โ€” forwardRef, or restructure to break the cycle.

โ† Express vs NestJS | Index | Next: DTOs & Validation โ†’