Skip to main content
Dependency Injection

Chapter 2: Dependency Injection & Modules

Dependency Injection (DI) is like having a smart assistant who brings you the tools you need, when you need them, instead of you having to build or find them yourself. In NestJS, DI is the backbone of modular, testable, and maintainable code. This chapter explores DI in depth, including provider types, scopes, custom providers, advanced module patterns, and real-world testing.

2.1 What is Dependency Injection?

Dependency Injection is a design pattern where dependencies (services, repositories, etc.) are provided to a class, rather than the class creating them itself. This allows for easier testing, maintenance, and flexibility.

Understanding the Problem DI Solves

Without Dependency Injection:
// ❌ Bad: Hard-coded dependency
export class UsersService {
  private logger = new LoggerService();  // Hard to test, hard to swap
  
  createUser(userData: any) {
    this.logger.log('Creating user');
    // ...
  }
}
Problems:
  • Can’t easily test UsersService in isolation
  • Can’t swap LoggerService for a different implementation
  • Tight coupling between UsersService and LoggerService
  • Hard to mock dependencies in tests
With Dependency Injection:
// ✅ Good: Dependency injected
@Injectable()
export class UsersService {
  constructor(private logger: LoggerService) {}  // Injected, not created
  
  createUser(userData: any) {
    this.logger.log('Creating user');
    // ...
  }
}
Benefits:
  • Easy to test (can inject a mock logger)
  • Can swap implementations easily
  • Loose coupling between classes
  • NestJS manages the lifecycle
Analogy:
Imagine a chef (your class) who needs ingredients (dependencies). Instead of growing or buying them, a supplier (NestJS DI system) delivers fresh ingredients to the kitchen. The chef can focus on cooking, and you can swap suppliers for testing or upgrades.

How NestJS DI Works

NestJS uses a dependency injection container that:
  1. Registers providers when modules are loaded
  2. Resolves dependencies by analyzing constructor parameters
  3. Creates instances based on provider scope
  4. Manages lifecycle (singleton, request-scoped, transient)
The DI Container Flow:
1. Module loads → Providers registered
2. Class needs dependency → Container looks it up
3. Dependency found → Container creates/retrieves instance
4. Instance injected → Class can use dependency
Benefits of DI:
  • Decouples components: Swap implementations easily
  • Enables easy mocking: Inject fake services in tests
  • Supports modular architecture: Plug-and-play modules
  • Manages lifecycle: Handles creation and destruction
  • Reduces boilerplate: No manual instantiation needed

2.2 Providers in NestJS

Providers are the backbone of DI in NestJS. Any class annotated with @Injectable() can be injected as a dependency. Providers can be services, repositories, factories, or even values.

What is a Provider?

A provider is anything that can be injected as a dependency. The most common type is a service, but providers can also be:
  • Services (business logic)
  • Repositories (data access)
  • Factories (functions that create instances)
  • Values (constants, configuration objects)
  • Classes (can be swapped for other implementations)

Basic Provider Example

import { Injectable } from '@nestjs/common';

@Injectable()  // This decorator makes it a provider
export class LoggerService {
  log(message: string) {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }

  error(message: string, error?: Error) {
    console.error(`[${new Date().toISOString()}] ERROR: ${message}`, error);
  }
}

Injecting a Provider

import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.service';

@Injectable()
export class AppService {
  // Dependency injected via constructor
  constructor(private logger: LoggerService) {}

  doWork() {
    this.logger.log('Work started');
    // ... do work
    this.logger.log('Work completed');
  }
}
How It Works:
  1. NestJS sees LoggerService in the constructor
  2. Looks up LoggerService in the DI container
  3. Creates an instance (or reuses existing one based on scope)
  4. Injects it into AppService

Registering Providers

Providers must be registered in a module:
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { LoggerService } from './logger.service';

@Module({
  providers: [
    AppService,      // Registered as provider
    LoggerService,   // Registered as provider
  ],
})
export class AppModule {}
Diagram: DI Flow
AppService needs LoggerService

Constructor parameter detected

NestJS DI Container looks up LoggerService

LoggerService found in providers array

Instance created/retrieved (based on scope)

Injected into AppService constructor

AppService can use LoggerService
Tip: Use @Injectable() on any class you want to inject elsewhere. Without it, NestJS won’t be able to inject the dependency.

2.3 Provider Scopes

NestJS supports three provider scopes that control how instances are created and shared. Understanding scopes is crucial for building correct applications.

Scope Types

1. Default (Singleton) Scope:
  • One instance per application
  • Created when the app starts
  • Shared across all requests
  • Most common and efficient
2. Request Scope:
  • New instance per HTTP request
  • Created when request starts
  • Destroyed when request ends
  • Useful for request-specific data
3. Transient Scope:
  • New instance every time it’s injected
  • Not shared between consumers
  • Useful for stateless, short-lived logic

Singleton Scope (Default)

@Injectable()  // Default is singleton
export class ConfigService {
  private config: any;

  constructor() {
    console.log('ConfigService created');  // Called once
    this.config = { apiKey: 'secret' };
  }

  get(key: string) {
    return this.config[key];
  }
}
Characteristics:
  • Created once when app starts
  • Same instance used everywhere
  • Efficient (no repeated creation)
  • State persists across requests
Use When:
  • Stateless services (most services)
  • Configuration services
  • Shared utilities
  • Database connections

Request Scope

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  private requestId: string;

  constructor() {
    this.requestId = Math.random().toString(36).substring(7);
    console.log(`RequestContextService created for request: ${this.requestId}`);
  }

  getRequestId() {
    return this.requestId;
  }
}
Characteristics:
  • New instance per HTTP request
  • Can access request object
  • Automatically cleaned up after request
  • More memory overhead
Use When:
  • Need request-specific data
  • User context services
  • Request logging
  • Multi-tenancy scenarios
Important: Request-scoped providers can’t be injected into singleton providers directly. You need to use REQUEST provider or make the consumer request-scoped too.

Transient Scope

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
  private id: string;

  constructor() {
    this.id = Math.random().toString(36).substring(7);
    console.log(`TransientService created: ${this.id}`);
  }

  getId() {
    return this.id;
  }
}
Characteristics:
  • New instance every injection
  • Not shared between consumers
  • Most memory overhead
  • Fully isolated
Use When:
  • Stateless helpers
  • Calculators
  • Validators
  • Transformers

Scope Comparison

ScopeInstancesLifecycleUse Case
Singleton1 per appApp lifetimeMost services
Request1 per requestRequest lifetimeRequest context
Transient1 per injectionInjection lifetimeStateless helpers
Diagram: Provider Scopes
Singleton:
  App Start → Instance Created → Shared Everywhere → App Shutdown

Request:
  Request 1 → Instance 1 Created → Used → Destroyed
  Request 2 → Instance 2 Created → Used → Destroyed

Transient:
  Injection 1 → Instance 1 Created → Used → Destroyed
  Injection 2 → Instance 2 Created → Used → Destroyed
Best Practices:
  • Use Singleton for most services (default)
  • Use Request scope only when you need request-specific data
  • Use Transient sparingly (higher memory cost)
  • Be aware of scope interactions (request-scoped can’t inject into singleton)

2.4 Custom Providers

Custom providers allow you to control how dependencies are created and injected. This is useful for configuration, factories, and swapping implementations.

Why Custom Providers?

Sometimes you need more control than a simple class:
  • Inject configuration values
  • Create instances with complex logic
  • Swap implementations (testing, different environments)
  • Integrate third-party libraries
  • Create async initialization

Value Provider

Inject a static value (e.g., config object, constants):
import { Module } from '@nestjs/common';

const CONFIG_TOKEN = 'CONFIG';

@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,  // Token to identify this provider
      useValue: {              // Static value to inject
        apiKey: 'secret-key',
        dbHost: 'localhost',
        dbPort: 5432,
      },
    },
  ],
})
export class ConfigModule {}
Using the Value Provider:
import { Injectable, Inject } from '@nestjs/common';

const CONFIG_TOKEN = 'CONFIG';

@Injectable()
export class ApiService {
  constructor(
    @Inject(CONFIG_TOKEN) private config: any,  // Inject using token
  ) {}

  makeRequest() {
    const apiKey = this.config.apiKey;
    // Use config...
  }
}
Use Cases:
  • Configuration objects
  • Constants
  • Environment-specific values
  • Third-party library instances

Factory Provider

Inject a value created by a function (can be async):
import { Module } from '@nestjs/common';

@Module({
  providers: [
{
      provide: 'DATABASE_CONNECTION',
  useFactory: async () => {
        // Can be async
        const connection = await createDatabaseConnection();
        return connection;
      },
      inject: [ConfigService],  // Inject dependencies into factory
    },
  ],
})
export class DatabaseModule {}
Factory with Dependencies:
@Module({
  providers: [
    ConfigService,
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: (config: ConfigService) => {
        // ConfigService is injected into factory
        return createConnection({
          host: config.get('DB_HOST'),
          port: config.get('DB_PORT'),
        });
      },
      inject: [ConfigService],  // Dependencies for factory
    },
  ],
})
export class DatabaseModule {}
Use Cases:
  • Async initialization
  • Complex object creation
  • Conditional logic
  • Integration with third-party libraries

Class Provider

Swap one class for another (e.g., for testing or different implementations):
// Base interface
export interface ILogger {
  log(message: string): void;
}

// Default implementation
@Injectable()
export class LoggerService implements ILogger {
  log(message: string) {
    console.log(message);
  }
}

// Alternative implementation
@Injectable()
export class AdvancedLoggerService implements ILogger {
  log(message: string) {
    // Log to file, database, etc.
    console.log(`[ADVANCED] ${message}`);
  }
}

// Module with class provider
@Module({
  providers: [
    {
      provide: LoggerService,           // Token (the class)
      useClass: AdvancedLoggerService,  // Implementation to use
    },
  ],
})
export class LoggerModule {}
Use Cases:
  • Swapping implementations (dev vs prod)
  • Testing (mock implementations)
  • Strategy pattern
  • A/B testing different implementations

Provider Token Best Practices

Use string tokens for values:
const CONFIG_TOKEN = 'CONFIG';  // String token

{
  provide: CONFIG_TOKEN,
  useValue: { ... },
}
Use class tokens for services:
{
  provide: LoggerService,  // Class as token
  useClass: AdvancedLoggerService,
}
Use symbol tokens for better type safety:
export const CONFIG_TOKEN = Symbol('CONFIG');

{
  provide: CONFIG_TOKEN,
  useValue: { ... },
}
Tip: Use custom providers for environment configs, third-party integrations, or when you need to mock dependencies in tests.

2.5 Injection Tokens

Tokens are used to identify providers in the DI container. Understanding tokens is essential for custom providers and advanced DI patterns.

Default Token (Class)

By default, the class itself is the token:
@Injectable()
export class UsersService {}

// In constructor, UsersService is both the type and the token
constructor(private usersService: UsersService) {}

String Tokens

Use string tokens for values and non-class providers:
const CONFIG_TOKEN = 'CONFIG';

@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,  // String token
      useValue: { apiKey: 'secret' },
    },
  ],
})
export class ConfigModule {}

// Inject using @Inject decorator
constructor(@Inject(CONFIG_TOKEN) private config: any) {}

Symbol Tokens

Symbols provide better type safety and avoid naming conflicts:
export const CONFIG_TOKEN = Symbol('CONFIG');

@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,  // Symbol token
      useValue: { apiKey: 'secret' },
    },
  ],
})
export class ConfigModule {}

// Inject using symbol
constructor(@Inject(CONFIG_TOKEN) private config: any) {}

Injection Token Patterns

Pattern 1: Interface Token (using string/symbol)
// Define token
export const I_USER_REPOSITORY = Symbol('IUserRepository');

// Interface
export interface IUserRepository {
  findById(id: number): Promise<User>;
}

// Implementation
@Injectable()
export class UserRepository implements IUserRepository {
  async findById(id: number) {
    // ...
  }
}

// Register with token
@Module({
  providers: [
    {
      provide: I_USER_REPOSITORY,
      useClass: UserRepository,
    },
  ],
})
export class UsersModule {}

// Inject using token
constructor(@Inject(I_USER_REPOSITORY) private userRepo: IUserRepository) {}
Best Practice: Use string or symbol tokens for values, and class tokens for services. Use interfaces with tokens for better abstraction.

2.6 Dynamic Modules

Dynamic modules allow you to configure modules at runtime, useful for libraries and shared infrastructure. They let you pass options when importing a module.

Why Dynamic Modules?

Static modules are fine when configuration doesn’t change, but sometimes you need:
  • Database connections with different configs
  • Feature flags
  • Environment-specific settings
  • Reusable libraries that need configuration

Basic Dynamic Module

import { Module, DynamicModule } from '@nestjs/common';

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
      global: true,  // Make it global (optional)
    };
  }
}
Using the Dynamic Module:
@Module({
  imports: [
    DatabaseModule.forRoot({
      host: 'localhost',
      port: 5432,
      database: 'mydb',
    }),
  ],
})
export class AppModule {}

Advanced: forRootAsync

For async configuration (e.g., loading from remote source):
@Module({})
export class DatabaseModule {
  static forRootAsync(options: {
    useFactory: (...args: any[]) => Promise<DatabaseOptions> | DatabaseOptions;
    inject?: any[];
  }): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}
Usage:
@Module({
  imports: [
    DatabaseModule.forRootAsync({
      useFactory: async (config: ConfigService) => {
        return {
          host: config.get('DB_HOST'),
          port: config.get('DB_PORT'),
        };
      },
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
Analogy:
Think of dynamic modules as “configurable blueprints”—you can build the same house with different paint, windows, or doors, depending on your needs.
Common Patterns:
  • forRoot() - Synchronous configuration
  • forRootAsync() - Asynchronous configuration
  • forFeature() - Feature-specific configuration

2.7 Module Architecture & Advanced Patterns

Modules group related providers and controllers. Understanding module patterns helps you build scalable, maintainable applications.

Feature Modules

Organize your code by business domain (e.g., UsersModule, OrdersModule). Each module should encapsulate its own providers, controllers, and entities. Example: Users Feature Module
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';

@Module({
  controllers: [UsersController],
  providers: [UsersService, UserRepository],
  exports: [UsersService],  // Export for other modules
})
export class UsersModule {}
Benefits:
  • Clear boundaries
  • Easy to find related code
  • Can be developed independently
  • Easy to test in isolation

Shared Modules

Shared modules export common providers (e.g., ConfigService, LoggerService) for use in other modules.
// shared/shared.module.ts
import { Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { ConfigService } from './config.service';

@Module({
  providers: [LoggerService, ConfigService],
  exports: [LoggerService, ConfigService],  // Export for other modules
})
export class SharedModule {}
Using Shared Module:
// users/users.module.ts
import { Module } from '@nestjs/common';
import { SharedModule } from '../shared/shared.module';
import { UsersService } from './users.service';

@Module({
  imports: [SharedModule],  // Import shared module
  providers: [UsersService],
})
export class UsersModule {
  // UsersService can now inject LoggerService and ConfigService
}
Best Practice: Always use the exports array to make providers available outside the module. Only export what’s needed.

Global Modules

Make a module available everywhere without importing:
import { Module, Global } from '@nestjs/common';

@Global()  // Makes module global
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}
Use Sparingly: Global modules can make dependencies unclear. Use only for truly global services (like ConfigService).

Re-exporting Modules

You can re-export entire modules to compose features:
// core/core.module.ts
import { Module } from '@nestjs/common';
import { SharedModule } from '../shared/shared.module';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [SharedModule, DatabaseModule],
  exports: [SharedModule, DatabaseModule],  // Re-export
})
export class CoreModule {}
Usage:
@Module({
  imports: [CoreModule],  // Gets both SharedModule and DatabaseModule
})
export class AppModule {}

Handling Circular Dependencies

Circular dependencies occur when two providers depend on each other. Use forwardRef to resolve them:
// auth/auth.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [forwardRef(() => UsersModule)],  // Forward reference
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

// users/users.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UsersService } from './users.service';
import { AuthModule } from '../auth/auth.module';

@Module({
  imports: [forwardRef(() => AuthModule)],  // Forward reference
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
Also use forwardRef in providers:
@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
  ) {}
}
Best Practice: Avoid circular dependencies when possible. They indicate a design issue. If you must use them, forwardRef is the solution. Diagram: Module Relationships
AppModule
  ├── UsersModule
  │   ├── UsersController
  │   ├── UsersService
  │   └── UserRepository
  ├── AuthModule
  │   ├── AuthController
  │   └── AuthService (uses UsersService)
  └── SharedModule (exports LoggerService, ConfigService)
      └── Used by UsersModule and AuthModule
Best Practices:
  • Keep modules focused and cohesive
  • Export only what’s needed
  • Use shared modules for cross-cutting concerns
  • Avoid circular dependencies; use forwardRef only when necessary
  • Use global modules sparingly

2.8 Real-World Example: Config Service & Advanced Providers

Let’s see how to build a flexible configuration system using DI and custom providers.

Basic Config Service

// config/config.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ConfigService {
  private readonly config = {
    apiKey: process.env.API_KEY,
    dbHost: process.env.DB_HOST || 'localhost',
    dbPort: parseInt(process.env.DB_PORT || '5432', 10),
    logLevel: process.env.LOG_LEVEL || 'info',
  };

  get<T = any>(key: string): T {
    return this.config[key];
  }

  getOrThrow<T = any>(key: string): T {
    const value = this.config[key];
    if (value === undefined) {
      throw new Error(`Configuration key "${key}" is required`);
    }
    return value;
  }
}

Config Module

// config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()  // Make it global so it's available everywhere
@Module({
  providers: [ConfigService],
  exports: [ConfigService],
})
export class ConfigModule {}

Using Config Service

// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '../config/config.service';

@Injectable()
export class UsersService {
  constructor(private config: ConfigService) {}

  async connectToDatabase() {
    const dbHost = this.config.get<string>('dbHost');
    const dbPort = this.config.get<number>('dbPort');
    // Connect...
  }
}

Advanced: Async Factory Provider for Config

// config/config.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';

@Global()
@Module({
  providers: [
    {
      provide: ConfigService,
      useFactory: async () => {
        // Can load from remote source, file, etc.
        const config = await loadConfigFromRemote();
        return new ConfigService(config);
      },
    },
  ],
  exports: [ConfigService],
})
export class ConfigModule {}

Using String Token for Config

// config/config.module.ts
const CONFIG_TOKEN = 'CONFIG';

@Module({
  providers: [
    {
      provide: CONFIG_TOKEN,
      useFactory: async () => {
        return await loadConfig();
      },
    },
  ],
  exports: [CONFIG_TOKEN],
})
export class ConfigModule {}

// Usage
import { Inject } from '@nestjs/common';
const CONFIG_TOKEN = 'CONFIG';

@Injectable()
export class ApiService {
  constructor(@Inject(CONFIG_TOKEN) private config: any) {}
}

2.9 Testing Providers & Modules

Testing DI is easy with Nest’s testing utilities. Use Test.createTestingModule to mock providers and test modules in isolation.

Basic Service Test

import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from './config.service';

describe('ConfigService', () => {
  let service: ConfigService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ConfigService],
    }).compile();

    service = module.get<ConfigService>(ConfigService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should return config value', () => {
    process.env.API_KEY = 'test-key';
    const value = service.get('apiKey');
    expect(value).toBe('test-key');
  });
});

Mocking Providers

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { ConfigService } from './config.service';

describe('UsersService', () => {
  let service: UsersService;
  let configService: ConfigService;

  beforeEach(async () => {
    // Create mock
    const mockConfigService = {
      get: jest.fn().mockReturnValue('mocked-value'),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: ConfigService,
          useValue: mockConfigService,  // Use mock instead of real
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    configService = module.get<ConfigService>(ConfigService);
  });

  it('should use mocked config', () => {
    service.doSomething();
    expect(configService.get).toHaveBeenCalled();
  });
});

Testing with Modules

import { Test, TestingModule } from '@nestjs/testing';
import { UsersModule } from './users.module';
import { UsersService } from './users.service';

describe('UsersModule', () => {
  let module: TestingModule;

  beforeEach(async () => {
    module = await Test.createTestingModule({
      imports: [UsersModule],  // Import the actual module
    }).compile();
  });

  it('should have UsersService', () => {
    const service = module.get<UsersService>(UsersService);
    expect(service).toBeDefined();
  });
});

Testing Custom Providers

describe('Service with Custom Provider', () => {
  let service: ApiService;

  beforeEach(async () => {
const module = await Test.createTestingModule({
  providers: [
        ApiService,
        {
          provide: 'CONFIG',
          useValue: { apiKey: 'test-key' },
        },
  ],
}).compile();

    service = module.get<ApiService>(ApiService);
  });

  it('should use custom provider', () => {
    expect(service).toBeDefined();
  });
});
Tip: Always mock external dependencies (like databases or APIs) in your tests for speed and reliability. Use real implementations only in integration tests.

2.10 Common Pitfalls and Solutions

Pitfall 1: Forgetting to Export Providers

// ❌ Bad: Service not exported
@Module({
  providers: [UsersService],
  // Missing exports!
})
export class UsersModule {}

// ✅ Good: Export what others need
@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

Pitfall 2: Circular Dependencies

// ❌ Bad: Direct circular dependency
@Module({
  imports: [AuthModule],  // AuthModule imports UsersModule
})
export class UsersModule {}

// ✅ Good: Use forwardRef
@Module({
  imports: [forwardRef(() => AuthModule)],
})
export class UsersModule {}

Pitfall 3: Request-Scoped in Singleton

// ❌ Bad: Can't inject request-scoped into singleton
@Injectable()
export class SingletonService {
  constructor(private requestService: RequestService) {}  // Error!
}

// ✅ Good: Make consumer request-scoped or use REQUEST provider
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  constructor(private requestService: RequestService) {}  // OK
}

Pitfall 4: Missing @Injectable()

// ❌ Bad: Missing decorator
export class UsersService {}

// ✅ Good: Add decorator
@Injectable()
export class UsersService {}

2.11 Summary

You’ve learned how dependency injection powers modular, testable, and scalable applications in NestJS: Key Concepts:
  • Providers: Classes, values, or factories that can be injected
  • Scopes: Singleton (default), Request, and Transient
  • Custom Providers: Value, Factory, and Class providers
  • Injection Tokens: Class, string, or symbol tokens
  • Dynamic Modules: Configurable modules with forRoot() and forRootAsync()
  • Module Patterns: Feature modules, shared modules, global modules
Best Practices:
  • Use singleton scope for most services
  • Export only what’s needed from modules
  • Use custom providers for configuration and third-party integrations
  • Avoid circular dependencies when possible
  • Mock dependencies in unit tests
Next Chapter: Learn about controllers, routing, request handling, validation, middleware, guards, and interceptors in depth.