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:- Can’t easily test
UsersServicein isolation - Can’t swap
LoggerServicefor a different implementation - Tight coupling between
UsersServiceandLoggerService - Hard to mock dependencies in tests
- Easy to test (can inject a mock logger)
- Can swap implementations easily
- Loose coupling between classes
- NestJS manages the lifecycle
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:- Registers providers when modules are loaded
- Resolves dependencies by analyzing constructor parameters
- Creates instances based on provider scope
- Manages lifecycle (singleton, request-scoped, transient)
- 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
Injecting a Provider
- NestJS sees
LoggerServicein the constructor - Looks up
LoggerServicein the DI container - Creates an instance (or reuses existing one based on scope)
- Injects it into
AppService
Registering Providers
Providers must be registered in a module:@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
- New instance per HTTP request
- Created when request starts
- Destroyed when request ends
- Useful for request-specific data
- New instance every time it’s injected
- Not shared between consumers
- Useful for stateless, short-lived logic
Singleton Scope (Default)
- Created once when app starts
- Same instance used everywhere
- Efficient (no repeated creation)
- State persists across requests
- Stateless services (most services)
- Configuration services
- Shared utilities
- Database connections
Request Scope
- New instance per HTTP request
- Can access request object
- Automatically cleaned up after request
- More memory overhead
- Need request-specific data
- User context services
- Request logging
- Multi-tenancy scenarios
REQUEST provider or make the consumer request-scoped too.
Transient Scope
- New instance every injection
- Not shared between consumers
- Most memory overhead
- Fully isolated
- Stateless helpers
- Calculators
- Validators
- Transformers
Scope Comparison
| Scope | Instances | Lifecycle | Use Case |
|---|---|---|---|
| Singleton | 1 per app | App lifetime | Most services |
| Request | 1 per request | Request lifetime | Request context |
| Transient | 1 per injection | Injection lifetime | Stateless helpers |
- 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):- Configuration objects
- Constants
- Environment-specific values
- Third-party library instances
Factory Provider
Inject a value created by a function (can be async):- 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):- Swapping implementations (dev vs prod)
- Testing (mock implementations)
- Strategy pattern
- A/B testing different implementations
Provider Token Best Practices
Use string tokens for values: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:String Tokens
Use string tokens for values and non-class providers:Symbol Tokens
Symbols provide better type safety and avoid naming conflicts:Injection Token Patterns
Pattern 1: Interface Token (using string/symbol)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
Advanced: forRootAsync
For async configuration (e.g., loading from remote source):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 configurationforRootAsync()- Asynchronous configurationforFeature()- 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- 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.exports array to make providers available outside the module. Only export what’s needed.
Global Modules
Make a module available everywhere without importing:Re-exporting Modules
You can re-export entire modules to compose features:Handling Circular Dependencies
Circular dependencies occur when two providers depend on each other. UseforwardRef to resolve them:
forwardRef is the solution.
Diagram: Module Relationships
- Keep modules focused and cohesive
- Export only what’s needed
- Use shared modules for cross-cutting concerns
- Avoid circular dependencies; use
forwardRefonly 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 Module
Using Config Service
Advanced: Async Factory Provider for Config
Using String Token for Config
2.9 Testing Providers & Modules
Testing DI is easy with Nest’s testing utilities. UseTest.createTestingModule to mock providers and test modules in isolation.
Basic Service Test
Mocking Providers
Testing with Modules
Testing Custom Providers
2.10 Common Pitfalls and Solutions
Pitfall 1: Forgetting to Export Providers
Pitfall 2: Circular Dependencies
Pitfall 3: Request-Scoped in Singleton
Pitfall 4: Missing @Injectable()
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()andforRootAsync() - Module Patterns: Feature modules, shared modules, global modules
- 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