NestJS Dependency Injection Pitfalls: A Tale of Duplicate Providers
The Scenario
Today, I encountered a classic dependency injection issue while integrating a new tournament system with artifacts tracking in our Mercury Bot project. The error was subtle but instructive:
[Nest] 2991604 - 02/03/2025, 6:01:58 PM ERROR [ExceptionHandler] Cannot read properties of undefined (reading 'get')
TypeError: Cannot read properties of undefined (reading 'get')
The Root Cause
The issue stemmed from a common anti-pattern in NestJS applications: duplicate provider registrations across multiple modules. Specifically:
- The same providers (ArtifactsService, Octokit) were registered in multiple modules
- Queue configurations were redundantly declared
- Module dependencies weren't properly structured
The Wrong Approach
Here's what the problematic code looked like:
@Module({
imports: [/* ... */],
providers: [
// Duplicate providers that were already available through ArtifactsModule
ArtifactsService,
Octokit,
// Other providers
]
})
The issue here is that we were trying to provide the same service in multiple places, leading to dependency injection conflicts and potentially undefined behavior.
The Solution
The solution involved following these key principles:
-
Single Responsibility: Let each module manage its own providers
// ArtifactsModule handles its own providers
@Module({
providers: [ArtifactsService, Octokit],
exports: [ArtifactsService]
}) -
Proper Module Imports: Instead of re-declaring providers, import the modules that provide them
@Module({
imports: [
ArtifactsModule,
TournamentModule
]
}) -
Queue Registration: Let each module handle its own queue registration
// In the module that owns the queue
BullModule.registerQueue({
name: QueueName.TOURNAMENT,
});
Key Takeaways
-
Provider Uniqueness: A provider should be declared in exactly one module and exported if needed elsewhere.
-
Module Boundaries: Respect module boundaries - if you need functionality from another module, import that module rather than recreating its providers.
-
Configuration Management: Global configurations (like ConfigModule) should be configured once and marked as global if needed everywhere.
-
Queue Registration: Each module should register only the queues it directly uses, avoiding duplicate registrations.
Best Practices Moving Forward
-
Before adding a provider to a module, check if it's already available through an imported module.
-
Use the
@Injectable()decorator'sscopeproperty when you need multiple instances. -
Keep module dependencies clean and explicit - avoid circular dependencies.
-
Document module dependencies and exports clearly.
Real-World Impact
This issue manifested in our tournament system integration, where the ConfigService was undefined due to provider conflicts. The fix not only resolved the immediate error but also:
- Reduced code duplication
- Clarified module responsibilities
- Improved application startup performance
- Made the codebase more maintainable
Conclusion
Dependency injection is a powerful feature of NestJS, but it requires careful attention to provider uniqueness and module boundaries. When in doubt, follow the principle of single responsibility and let each module manage its own providers.
Remember: Just because TypeScript compiles doesn't mean your dependency injection is configured correctly. Always test your module integration thoroughly and pay attention to provider scope and registration.
This post is part of our ongoing series about real-world lessons learned while building Mercury Bot, a sophisticated market analysis system.
