<link rel="stylesheet" href="styles-3GBANQL6.css">
← Back to blog

NestJS

Avoiding Circular Dependencies in NestJS Applications

Avoiding Circular Dependencies in NestJS Applications

Introduction

As you develop your NestJS server application, things usually go smoothly until you encounter the dreaded circular dependency error:

[Nest] 116557  - 03/07/2023, 2:12:40 PM   ERROR [ExceptionHandler] 
Nest cannot create the ExampleModule instance. 
The module at index [0] of the ExampleModule "imports" array is undefined.
    

This error can seem daunting at first, but it often points to a circular dependency between your modules or services. In this post, we'll break down what circular dependencies are, the different types, how to detect them, and most importantly, how to avoid them!

What is a Circular Dependency?

A circular dependency occurs when two or more modules, services, or files depend on each other to function, creating a loop that cannot be resolved easily. In NestJS, there are three primary types of circular dependencies:

  • Circular file imports: File A imports File B, and File B imports File A.
  • Circular module imports: Module A imports Module B, and Module B imports Module A.
  • Circular constructor dependencies: Service A depends on Service B, and vice versa.

Circular Module Imports

Let’s look at an example. Consider the following two modules:


// foo.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { FooService } from './foo.service';
import { BarModule } from '../bar/bar.module';

@Module({
  imports: [forwardRef(() => BarModule)],
  providers: [FooService],
  exports: [FooService],
})
export class FooModule {}
    

// bar.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { BarService } from './bar.service';
import { FooModule } from '../foo/foo.module';

@Module({
  imports: [forwardRef(() => FooModule)],
  providers: [BarService],
  exports: [BarService],
})
export class BarModule {}
    

Here, both modules import each other, causing a circular dependency. We resolve this by using the forwardRef() function, which delays the import resolution until runtime, breaking the circular reference.

Circular Constructor Injections

Another common scenario is circular constructor injections. For example:


// foo.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { BarService } from '../bar/bar.service';

@Injectable()
export class FooService {
  constructor(
    @Inject(forwardRef(() => BarService))
    private readonly barService: BarService,
  ) {}
}
    

// bar.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { FooService } from '../foo/foo.service';

@Injectable()
export class BarService {
  constructor(
    @Inject(forwardRef(() => FooService))
    private readonly fooService: FooService,
  ) {}
}
    

In this example, FooService depends on BarService, and vice versa. Without forwardRef(), NestJS would fail to resolve these dependencies. However, while this solution works, it’s better to avoid circular dependencies by rethinking the design if possible.

Detecting Circular Dependencies

Now that you know how circular dependencies occur, let’s look at how to find them. A great tool for this is madge, which can detect circular dependencies in your project:


npm install -D madge
npx madge --circular src/main.ts
    

This will output something like:

✖ Found 2 circular dependencies!
1) bar/bar.service.ts > foo/foo.service.ts
2) bar/bar.module.ts > foo/foo.module.ts
    

Now you can see exactly where the circular dependencies are in your code and refactor them as needed.

Why You Should Avoid Circular Dependencies

Circular dependencies are often a sign of tightly coupled code, which can lead to maintenance difficulties and performance issues. Specifically, they can cause:

  • Race conditions: Dependencies are resolved in parallel, so circular dependencies can lead to undefined values.
  • Fragile architecture: Tightly coupled code is harder to refactor, test, and reuse across different parts of your application.

Refactoring to Avoid Circular Dependencies

Instead of relying on forwardRef(), consider whether you can restructure your modules and services to avoid circular dependencies entirely. For example:

Solution 1: Extract shared logic

If both FooService and BarService need the same functionality, extract it into a new service or utility that both can import:


// shared.service.ts
@Injectable()
export class SharedService {
  commonMethod() { /* logic here */ }
}
    

Now, FooService and BarService can both inject SharedService instead of depending on each other.

Solution 2: Decouple modules

If possible, decouple modules so they do not directly depend on each other. For example, create a new mediator or event-driven system where modules communicate indirectly.

Conclusion

Circular dependencies can be challenging, but they’re also an opportunity to rethink the architecture of your application. Use tools like madge to detect them and always prefer refactoring over using forwardRef() unless absolutely necessary. Cleaner, decoupled code is easier to maintain, test, and scale!

LET'S BUILD WHAT'S NEXT

Ready to turn your idea into a real solution?

Let's discuss how we can help you create value with technology.

Let's Talk