Skip to content

Fix: NestJS Circular Dependency — forwardRef and Module Design

FixDevs ·

Quick Answer

How to fix NestJS circular dependency errors — using forwardRef, restructuring module dependencies, extracting shared services, and understanding the NestJS module system.

The Error

NestJS throws a circular dependency error on startup:

[Nest] ERROR [ExceptionHandler] Nest cannot create the module instance.
Caught a "Circular dependency" error while processing provider "UserService".
Please, make sure that each side of a circular dependency has been decorated with an "forwardRef()" decorator.

Or a less obvious dependency loop:

ERROR [ExceptionHandler] A circular dependency between modules has been detected.
Please, make sure that each side of a "forwardRef()" decorator is used.

The modules that form this cycle:
UsersModule -> PostsModule -> UsersModule

Or NestJS silently creates providers as undefined:

TypeError: Cannot read properties of undefined (reading 'findUser')
// userService is undefined because of unresolved circular dep

Why This Happens

Circular dependencies occur when two or more providers or modules depend on each other, forming a cycle:

  • Provider circular dependencyUserService injects PostService, and PostService also injects UserService. NestJS can’t instantiate either because each requires the other to exist first.
  • Module circular dependencyUsersModule imports PostsModule, and PostsModule imports UsersModule. Same deadlock at the module level.
  • Indirect circular dependencyABCA. The cycle may span three or more classes, making it harder to spot.
  • Missing forwardRef() — even when the circular dep is intentional and unavoidable, both sides need forwardRef() so NestJS can resolve them lazily.

Fix 1: Use forwardRef for Provider Circular Dependencies

forwardRef() tells NestJS to use a lazy reference — the actual class is resolved after all providers are registered:

// WRONG — direct circular injection, NestJS can't resolve
// users.service.ts
@Injectable()
export class UserService {
  constructor(private postService: PostService) {}  // ← Circular
}

// posts.service.ts
@Injectable()
export class PostService {
  constructor(private userService: UserService) {}  // ← Circular
}
// CORRECT — use forwardRef() on BOTH sides
// users.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { PostService } from '../posts/post.service';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => PostService))
    private postService: PostService,
  ) {}

  async getUserWithPosts(userId: number) {
    const posts = await this.postService.findByAuthor(userId);
    return { userId, posts };
  }
}
// posts.service.ts
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { UserService } from '../users/user.service';

@Injectable()
export class PostService {
  constructor(
    @Inject(forwardRef(() => UserService))
    private userService: UserService,
  ) {}

  async findByAuthor(userId: number) {
    const user = await this.userService.findOne(userId);
    return this.postRepository.find({ where: { authorId: user.id } });
  }
}

Common Mistake: Adding forwardRef() only on one side. Both sides of the circular dependency must use forwardRef(). If only one side uses it, NestJS still can’t resolve the cycle.

Fix 2: Use forwardRef for Module Circular Dependencies

When two modules import each other, use forwardRef() on both module imports:

// WRONG — direct circular module import
// users.module.ts
@Module({
  imports: [PostsModule],   // ← Circular
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}

// posts.module.ts
@Module({
  imports: [UsersModule],   // ← Circular
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}
// CORRECT — forwardRef() on both module imports
// users.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { PostsModule } from '../posts/posts.module';

@Module({
  imports: [forwardRef(() => PostsModule)],   // Lazy module reference
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}
// posts.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [forwardRef(() => UsersModule)],   // Lazy module reference
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}

Fix 3: Refactor to Break the Cycle (Preferred)

forwardRef() is a workaround. The better solution is redesigning the dependency structure to eliminate the cycle. Circular dependencies often indicate a violation of the Single Responsibility Principle — two modules are too tightly coupled.

Extract a shared service into a separate module:

Before (circular):
UsersModule ←→ PostsModule

After (no cycle):
UsersModule → SharedModule
PostsModule → SharedModule
// shared/shared.module.ts — contains logic needed by both modules
@Module({
  providers: [NotificationService, AuditService],
  exports: [NotificationService, AuditService],
})
export class SharedModule {}

// users.module.ts — imports SharedModule, not PostsModule
@Module({
  imports: [SharedModule],
  providers: [UserService],
  exports: [UserService],
})
export class UsersModule {}

// posts.module.ts — imports SharedModule, not UsersModule
@Module({
  imports: [SharedModule],
  providers: [PostService],
  exports: [PostService],
})
export class PostsModule {}

Move shared logic to a base service:

// Before — UserService calls PostService, PostService calls UserService
// After — both call a database service directly without importing each other

// database/database.service.ts
@Injectable()
export class DatabaseService {
  async findUserById(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }

  async findPostsByAuthor(authorId: number): Promise<Post[]> {
    return this.postRepository.find({ where: { authorId } });
  }
}

Use events to decouple services:

// Instead of PostService calling UserService directly,
// emit an event that UserService listens to
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class PostService {
  constructor(private eventEmitter: EventEmitter2) {}

  async createPost(data: CreatePostDto) {
    const post = await this.postRepository.save(data);
    // Emit event instead of calling UserService directly
    this.eventEmitter.emit('post.created', { postId: post.id, userId: data.authorId });
    return post;
  }
}

@Injectable()
export class UserService {
  @OnEvent('post.created')
  async handlePostCreated(event: { postId: number; userId: number }) {
    await this.updateUserPostCount(event.userId);
  }
}

Fix 4: Detect Circular Dependencies Early

NestJS provides a built-in dependency graph visualization. Use it during development to spot cycles before they cause runtime errors:

# Install NestJS CLI
npm install -g @nestjs/cli

# Generate a dependency graph (outputs a JSON or DOT graph)
nest info

# For visual graph — use NestJS DevTools
npm install @nestjs/devtools-integration
// main.ts — enable DevTools in development
import { NestFactory } from '@nestjs/core';
import { DevtoolsModule } from '@nestjs/devtools-integration';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    snapshot: true,  // Required for DevTools
  });
  await app.listen(3000);
}
bootstrap();

Simple manual cycle detection — trace the dependency chain:

UserService → PostService → CommentService → UserService  ← cycle detected

Draw out your service dependencies. Any graph with a path from a node back to itself is a cycle.

Use eslint-plugin-import to detect circular imports at the TypeScript level:

npm install --save-dev eslint-plugin-import
// .eslintrc.json
{
  "plugins": ["import"],
  "rules": {
    "import/no-cycle": ["error", { "maxDepth": 3 }]
  }
}

Fix 5: Lazy-Load Services with ModuleRef

For cases where you only need a service in specific methods (not in the constructor), use ModuleRef to resolve it lazily:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { PostService } from '../posts/post.service';

@Injectable()
export class UserService implements OnModuleInit {
  private postService: PostService;

  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    // Resolve the service after all modules are initialized
    this.postService = this.moduleRef.get(PostService, { strict: false });
  }

  async getUserWithPosts(userId: number) {
    const posts = await this.postService.findByAuthor(userId);
    return { userId, posts };
  }
}

ModuleRef.get() with strict: false searches the entire application context, not just the current module scope. Without this flag, it only looks in the current module.

For request-scoped providers, use resolve() instead of get():

async someMethod() {
  const postService = await this.moduleRef.resolve(PostService);
  return postService.findAll();
}

Fix 6: Structure Modules to Avoid Cycles

A module hierarchy that naturally avoids cycles:

AppModule
├── CoreModule (no deps on feature modules)
│   ├── DatabaseModule
│   ├── LoggerModule
│   └── ConfigModule
├── SharedModule (depends on CoreModule only)
│   ├── EmailService
│   ├── NotificationService
│   └── AuditService
├── UsersModule (depends on Core + Shared)
│   └── UserService
└── PostsModule (depends on Core + Shared + Users)
    └── PostService

Rules that prevent circular dependencies:

  1. Core modules don’t import feature modules.
  2. Shared modules only import core modules.
  3. Feature modules import core and shared — never each other directly.
  4. If two feature modules need to communicate, use events (EventEmitter2) or extract the shared logic into SharedModule.

Barrel exports from module boundaries:

// users/index.ts — public API of the UsersModule
export { UserService } from './user.service';
export { User } from './entities/user.entity';
export { CreateUserDto } from './dto/create-user.dto';

// Other modules import from the barrel, not internal files
// This makes dependencies explicit and easier to audit
import { UserService } from '../users';  // Clean boundary

Still Not Working?

Indirect cycles are harder to spot. If A → B → C → A, adding forwardRef() to only A and C won’t help — you need to trace the full cycle.

Check for accidental self-injection — a service that injects itself:

@Injectable()
export class UserService {
  constructor(
    private userService: UserService,  // ← Self-injection: always a circular dep
  ) {}
}

forwardRef() doesn’t work with all provider types. It works with class providers but may behave unexpectedly with factory providers. Prefer extracting shared logic over using forwardRef() with factories.

Verify the exported providers in each module. A circular import is harmless if neither module actually injects from the other — but the error appears if the import creates a circular module reference even without injecting providers. Use exports arrays to make only necessary providers available:

@Module({
  providers: [UserService, UserRepository],  // UserRepository is internal
  exports: [UserService],                    // Only UserService is exported
})
export class UsersModule {}

For related NestJS issues, see Fix: NestJS TypeORM QueryFailedError and Fix: Spring Boot Circular Dependency Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles