Diagnosing NestJS Circular Dependencies: Module vs Provider Cycles
Fix NestJS circular dependency errors by identifying module-level versus provider-level cycles, then applying the right extraction or forwardRef strategy.
06 May 2026, 20:59 UTC

Recognizable condition
Your NestJS application refuses to start. The bootstrap log contains a message such as Nest cannot create the module instance or Circular dependency detected, followed by a chain like UsersModule -> PostsModule -> UsersModule. In a less obvious form, the application starts but a service constructor receives undefined for a dependency that should have been injected.
Useful takeaway: circular dependencies in NestJS live in one of two layers. A module cycle happens when modules import each other through their imports arrays. A provider cycle happens when services inject each other through their constructors. The fix depends on which layer is involved, so the first diagnostic step is to identify the layer, not to add forwardRef everywhere.
Cause and diagnostic table
| Symptom | Likely cause | Where to look |
|---|---|---|
| Application crashes during bootstrap with a module chain | Module-level circular import | @Module imports arrays |
Application starts, but a service receives undefined at runtime | Provider-level circular injection | Constructor parameters and @Inject decorators |
| Unit tests fail while the application starts normally | Test module configuration differs from the application module | Test.createTestingModule imports |
forwardRef works locally but fails in production build | TypeScript module resolution or target mismatch | tsconfig.json |
Ordered checks
- Run the application with debug logging. From the project root, run
npm run start:dev -- --debug > bootstrap.log. This requires no special permissions. The placeholderbootstrap.logis the output file. Expected check: search the file for Circular dependency to locate the offending chain without scrolling through verbose output. Risk: the log is long; do not assume the first mention is the only cycle. - Identify the shortest cycle in the error. Copy the chain from the log or error message. If it lists modules, you have a module cycle. If it lists providers or services, you have a provider cycle.
- Inspect barrel files. Check whether any
index.tsre-exports modules that import each other. Barrel files hide cycles that are not visible in direct import statements. - Check for
@Global()modules. A global module can make a provider available everywhere and create implicit cycles that explicit imports would not. - Verify TypeScript configuration. Open
tsconfig.jsonand confirm module resolution settings match your deployment target. Mismatched settings can makeforwardRefbehave differently between local development and production builds.
Fixes matched to the finding
Module-level cycle: extract a shared module
If UsersModule imports PostsModule and PostsModule imports UsersModule, neither module should depend on the other. Extract the shared provider—often a repository, configuration service, or base entity—into a CoreModule or SharedModule, then import that module from both.
@Module({
providers: [UserRepository],
exports: [UserRepository],
})
export class CoreModule {}
Then replace the mutual imports with imports of CoreModule. Do not use forwardRef(() => OtherModule) at the module level; module-level forwardRef rarely solves the underlying design problem and can make the graph harder to reason about.
Provider-level cycle: use forwardRef on one side
If ServiceA injects ServiceB and ServiceB injects ServiceA, break the cycle by deferring resolution on one side:
import { Inject, forwardRef, Injectable } from '@nestjs/common';
import { ServiceB } from './service-b.service';
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB)) private readonly serviceB: ServiceB,
) {}
}
Run this code in the application source directory with normal user permissions. No build permission changes are required. Expected check: restart the application and confirm the constructor no longer receives undefined. Risk: forwardRef defers resolution but does not remove the coupling; it can make unit tests harder to set up and should be treated as a temporary structural fix, not a design feature.
Barrel-file cycle: import concrete files
If an index.ts file re-exports both sides of a cycle, replace barrel imports with direct file paths:
// Avoid
import { UsersModule } from './users';
// Use
import { UsersModule } from './users/users.module';
Expected check: run nest build from the project root and confirm the build completes without circular dependency warnings.
Test-only cycle: override the provider
If the cycle appears only in tests, configure the testing module with a mock instead of the real provider:
const module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(ServiceB)
.useValue({ method: jest.fn() })
.compile();
Expected check: run npm run test from the project root and confirm the previously failing test passes without changing application source code.
When to escalate to architecture review
- The cycle involves four or more modules, which usually indicates a domain boundary violation.
forwardRefchains exceed three levels, making the resolution order difficult to predict.- The cycle cannot be broken without reducing cohesion inside a module.
- Multiple teams own the modules in the cycle and need coordination before changing imports.
Limitations and how to verify the result
This guide covers common NestJS circular dependency patterns. It does not cover custom dependency injection containers or non-standard module loaders. forwardRef behavior can vary with TypeScript versions and project tsconfig.json settings; verify behavior in your target build before relying on it in production.
Practical verification: add an integration test that instantiates the full application graph:
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
If this compiles and the application bootstraps with npm run start:dev without dependency errors, the cycle is resolved. If the test still fails, repeat the ordered checks and inspect the full dependency graph for a cycle that unit tests did not surface.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.