Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
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). Without DI, the chef drives to the farm, picks tomatoes, milks the cow, and grinds the flour before cooking a single dish. With DI, a supplier (the NestJS DI container) delivers fresh ingredients to the kitchen door every morning. The chef can focus on cooking, and if you want to test the recipe with organic ingredients, you swap the supplier — the chef’s code never changes. This is exactly what happens when you mock a service in tests: you are swapping the supplier, not rewriting the chef.
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)
The singleton scope is the default and should be your go-to choice for 90%+ of providers. Think of it like a shared printer in an office — there is one printer, and everyone uses it. You do not buy a new printer for every print job.- Created once when app starts, lives until the app shuts down
- The exact same instance is injected everywhere it is requested
- Efficient (no repeated creation, no garbage collection churn)
- State persists across requests — be careful with mutable state
- Stateless services (most services)
- Configuration services
- Shared utilities
- Database connection pools
Request Scope
- New instance per HTTP request
- Can access request object
- Automatically cleaned up after request
- More memory overhead
- Need request-specific data (current user, tenant context)
- User context services
- Request logging with correlation IDs
- Multi-tenancy scenarios where each request targets a different database
REQUEST injection token to access request data indirectly.
Performance Warning: Every request-scoped provider is instantiated per request and garbage-collected when the request completes. In a high-traffic API handling thousands of requests per second, this creates significant overhead. Profile before adopting request scope widely.
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 |
- Does this provider need to store data that is specific to a single HTTP request (current user, tenant context, request correlation ID)? Use Request scope.
- Does each consumer need a guaranteed-unique instance with independent state (e.g., a builder that accumulates state across method calls)? Use Transient scope.
- Everything else? Use Singleton (the default). This covers 90%+ of providers.
| Consumer Scope | Can Inject Singleton? | Can Inject Request? | Can Inject Transient? |
|---|---|---|---|
| Singleton | Yes | No (throws error) | Yes (but same instance always) |
| Request | Yes | Yes | Yes |
| Transient | Yes | Yes (if consumer is also request-scoped) | Yes |
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?
The basic pattern — put@Injectable() on a class, list it in providers, inject via constructor — covers most cases. But sometimes you need more control. Think of custom providers as the “advanced settings” panel: you probably do not need them on day one, but they are essential for production applications.
Common scenarios where you need custom providers:
- Inject configuration values or constants (no class to decorate with
@Injectable()) - Create instances with complex or asynchronous initialization logic (database connections, SDK clients)
- Swap implementations based on environment (real payment gateway in production, mock in testing)
- Integrate third-party libraries that you do not control (cannot add
@Injectable()to their classes) - Create multiple instances of the same class with different configurations
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:| I need to… | Provider Type | Example |
|---|---|---|
| Inject a static value, constant, or config object | useValue | API keys, feature flags, config objects |
| Inject a class but swap the implementation | useClass | Mock services in test, strategy pattern |
| Create an instance with complex or async logic | useFactory | Database connections, SDK clients |
| Alias one provider to another token | useExisting | Provide the same instance under two tokens |
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 ordering from a menu with customization. A static module is a fixed combo meal — you get what you get. A dynamic module is “build your own” — you pick the database host, the port, whether to enable logging. The kitchen (NestJS) takes your order and assembles the module with your specific configuration. This is why most NestJS libraries (TypeORM, Config, Bull) use theCommon Patterns:forRoot()/forRootAsync()pattern — they need your configuration before they can set up their providers.
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, pause and ask: “Can I move the shared logic into a third module that both can import?” Nine times out of ten, the answer is yes, and the resulting architecture is cleaner. Use forwardRef only as a last resort for genuinely bidirectional relationships.
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 DI Edge Cases
Edge Case 1: Injecting into non-NestJS classes If you have a plain TypeScript class (not registered as a provider) that needs a service, you cannot use constructor injection. Options: (1) Register the class as a provider; (2) Pass the dependency as a method parameter; (3) UseModuleRef to resolve providers dynamically. Never use the app.get() hack to access providers outside the DI container — it breaks testability and creates hidden dependencies.
Edge Case 2: Multiple providers with the same interface
If you have PostgresUserRepository and MongoUserRepository, both implementing IUserRepository, you cannot register both under the same token. Use named tokens:
onModuleInit ordering between modules
If Module A imports Module B, Module B’s onModuleInit runs before Module A’s. But if Module A and Module B are both imported by AppModule with no dependency between them, the order is not guaranteed. If Module A’s initialization depends on Module B being ready, Module A must explicitly import Module B.
Edge Case 4: Dynamic module forRoot called multiple times
If two feature modules both import DatabaseModule.forRoot({ ... }) with different configs, you get two database connections (or an error, depending on the module implementation). This is why most infrastructure modules use global: true in forRoot() and provide a separate forFeature() for feature-specific registration. Follow this convention: forRoot() once in AppModule, forFeature() in each feature module.
Edge Case 5: Testing a provider that uses @Optional() dependencies
When a dependency is marked @Optional(), the provider works without it in production. But in tests, Test.createTestingModule() still tries to resolve it. If the optional dependency is from a module you did not import in your test, provide a useValue: undefined to explicitly skip it.
2.11 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
Interview Deep-Dive
Explain the three provider scopes in NestJS. When have you seen request-scoped providers cause production issues?
Explain the three provider scopes in NestJS. When have you seen request-scoped providers cause production issues?
Strong Answer:
- Singleton (default): one instance per application lifetime, shared across all requests. This is correct for 90%+ of providers — your
UsersService,ConfigService, database repositories are all singletons. The instance is created once during bootstrap and reused. - Request-scoped (
Scope.REQUEST): a new instance is created for every incoming HTTP request and garbage-collected when the response is sent. This is necessary when you need per-request state — the canonical example is a multi-tenant application where each request targets a different database based on the tenant ID in the JWT token. - Transient (
Scope.TRANSIENT): a new instance is created every time the provider is injected. If three different services inject a transient provider, they each get a different instance. This is useful for stateless utility classes where you explicitly want isolation. - The production issue I have seen with request-scoped providers is the “scope bubble” problem. If you make
TenantServicerequest-scoped, every provider that depends on it ALSO becomes request-scoped — NestJS forces this because a singleton cannot hold a reference to something that changes per request. In one project, making one service request-scoped cascaded through 15 other services, turning them all request-scoped. At 2,000 requests per second, this created 30,000 object instantiations per second, and GC pauses jumped from 5ms to 200ms, causing p99 latency to spike. The fix was to inject theREQUESTtoken directly into the singleton services that needed request context, rather than making a shared service request-scoped.
REQUEST injection token with @Inject(REQUEST) in a request-scoped provider, then pass the data explicitly as a method parameter to singleton services. Alternatively, use AsyncLocalStorage (Node.js built-in) to create a request context that any code in the call chain can access without DI. NestJS’s @nestjs/core provides ContextIdFactory for advanced use cases. The cleanest pattern I have used is a thin request-scoped RequestContextService that stores the user and tenant ID, and then the singleton services receive these as method parameters rather than injecting the context service directly. This keeps the scope bubble small — only one provider is request-scoped instead of the entire dependency chain.Walk me through how you would design a custom provider that connects to a third-party payment gateway. What provider pattern would you use and why?
Walk me through how you would design a custom provider that connects to a third-party payment gateway. What provider pattern would you use and why?
Strong Answer:
- I would use a factory provider (
useFactory) because the payment gateway client requires async initialization (API key validation, connection handshake) and depends on configuration values. - The provider definition would look like:
{ provide: 'PAYMENT_GATEWAY', useFactory: async (config: ConfigService) => { const client = new StripeClient(config.getOrThrow('STRIPE_SECRET_KEY')); await client.verify(); return client; }, inject: [ConfigService] }. Theinjectarray tells NestJS which providers to pass as arguments to the factory function. - I use
getOrThrowinstead ofgetbecause if the Stripe key is missing, I want the application to crash at startup, not serve requests that silently fail when they try to charge a card. This is the “fail fast” principle. - I use a string token (
'PAYMENT_GATEWAY') instead of a class token because theStripeClientclass comes from a third-party library — I cannot add@Injectable()to it. Consumers inject it with@Inject('PAYMENT_GATEWAY') private stripe: StripeClient. - For testing, I would register a mock payment gateway in the test module:
{ provide: 'PAYMENT_GATEWAY', useValue: { charge: jest.fn().mockResolvedValue({ id: 'ch_test', status: 'succeeded' }) } }. This lets me test the order processing flow without making real API calls. - In production, I would also add a health check that calls
stripe.balance.retrieve()to verify the connection is alive.
'PAYMENT_GATEWAY_US' and 'PAYMENT_GATEWAY_EU', each with its own factory provider reading different config keys. Alternatively, use a dynamic module pattern: PaymentModule.forRoot({ region: 'US', secretKey: 'STRIPE_US_KEY' }) that registers the provider with a region-specific token. The consumer then injects @Inject('PAYMENT_GATEWAY_US') or @Inject('PAYMENT_GATEWAY_EU'). If the region is determined at request time (multi-tenant), use a request-scoped factory that reads the region from the request context and returns the appropriate client.What is the difference between forRoot() and forFeature() in NestJS dynamic modules? Give a concrete example of when you would use each.
What is the difference between forRoot() and forFeature() in NestJS dynamic modules? Give a concrete example of when you would use each.
Strong Answer:
forRoot()is called once in the root module to configure a global, shared resource. Think of it as “set up the infrastructure.” For example,TypeOrmModule.forRoot({ host: 'localhost', ... })creates the database connection pool once and makes it available globally. You call this inAppModule.forFeature()is called in each feature module to register module-specific resources that depend on the global infrastructure.TypeOrmModule.forFeature([User, Post])tells TypeORM which entities this module uses and creates the repositories for them. You call this inUsersModule,PostsModule, etc.- The pattern works like a restaurant chain.
forRoot()is opening the restaurant (signing the lease, setting up the kitchen).forFeature()is adding items to the menu — each chef (module) adds their dishes, but they all share the same kitchen. - Under the hood,
forRoot()typically returns aDynamicModulewithglobal: true, which makes its exports available everywhere without importing.forFeature()returns a non-global module whose exports are available only to the importing module. - There is also
forRootAsync()for async configuration. This is essential in production because your database credentials come fromConfigService, which itself needs to be initialized first. The pattern is:TypeOrmModule.forRootAsync({ useFactory: (config) => ({ host: config.get('DB_HOST') }), inject: [ConfigService] }).
forRoot() twice creates two separate instances of the underlying resource. With TypeORM, that means two connection pools, which doubles your database connections and can exhaust the connection limit. Some packages guard against this (ConfigModule logs a warning), but most do not. The convention is: forRoot() once in AppModule, forFeature() everywhere else. If you need to share configuration across modules, make the root module global with global: true in the dynamic module definition.You inherit a NestJS codebase where two modules have a circular dependency. The previous developer used forwardRef() everywhere. How do you refactor this?
You inherit a NestJS codebase where two modules have a circular dependency. The previous developer used forwardRef() everywhere. How do you refactor this?
Strong Answer:
forwardRef()is a bandage, not a cure. It tells NestJS to “resolve this reference later” to break the initialization cycle, but it does not fix the underlying design problem — two modules that are conceptually coupled.- The first step is to draw the dependency graph. If
AuthModuleneedsUsersService(to validate credentials) andUsersModuleneedsAuthService(to hash passwords during registration), the circle is clear. The question is: what is the shared concern? - In this case, the shared concern is password hashing.
HashServicedoes not depend on eitherAuthModuleorUsersModule— it is a pure utility. Extract it into aSharedModuleorCryptoModule. NowAuthModuleimportsSharedModulefor password comparison, andUsersModuleimportsSharedModulefor password hashing. The circle is broken. - Another common pattern:
AuthModuleneeds to look up users, andUsersModuleneeds to check authentication for certain operations. The fix is to haveAuthModuledepend onUsersModule(import it to getUsersService), and for the auth-check inUsersModule, use guards (which are provided globally or by theAuthModule) instead of directly importingAuthModule. Guards do not create a module dependency — they are applied via decorators. - The general principle: if two modules depend on each other, either (1) extract shared logic into a third module, (2) merge the modules if they are truly inseparable, or (3) use events instead of direct calls (Service A emits an event, Service B listens to it, no import needed).
User has @OneToMany(() => Post) and Post has @ManyToOne(() => User), TypeORM uses forwardRef-style lazy arrow functions to handle the circular type reference. This is a TypeScript limitation, not a design smell. For NestJS module-level circular dependencies, I have never seen a case where refactoring was not the better solution.