DEV Community

Cover image for How to avoid circular dependencies in NestJS
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to avoid circular dependencies in NestJS

Written by Samuel Olusola✏️

Introduction

One of the beauties of NestJS is that it allows us to separate concerns within our applications. The NestJS architecture favors creating the logic layer as dependencies (i.e., services) that can be consumed by the access layers (i.e., controllers).

Even though NestJS has a built-in dependency injection system that takes care of resolving dependencies needed in different parts of our code, care must still be taken when working with dependencies. One of the common problems encountered in this regard is circular dependencies. NestJS code will not even compile if there is an unresolved circular dependency.

In this article, we will be learning about circular dependencies in NestJS, why they arise, and how we can avoid them. Rather than just presenting the workaround NestJS provides, I will be walking us through how to avoid circular dependencies by rethinking how we couple dependencies, which will be helpful for better architecture in our backend code.

Avoiding circular dependencies also ensures that our code is easier to understand and modify, because a circular dependency means there is tight coupling in our code.

Contents

What is a circular dependency?

In programming, a circular dependency occurs when two or more modules (or classes), directly or indirectly depend on one other. Say A, B, C and D are four modules, an example of direct circular dependency is ABA. Module A depends on module B which in turn depends on A.

An example of indirect circular dependency is ABCA. Module A depends on B which doesn’t depend on A directly, but later on in its dependency chain references A.

Note that the concept of circular dependency is not unique to NestJS, and in fact, the modules used here as example don’t even have to be NestJS modules. They simply represent the general idea of modules in programming, which refers to how we organize code.

Before we talk about circular dependencies (and how to avoid them) in NestJS, let us first discuss how the NestJS dependency injection system works.

Knowing how NestJS handles dependency injection will make it easier to understand how a circular reference can occur within our dependencies and why NestJS compile can’t compile until the circular reference is resolved.

The NestJS dependency injection system

In NestJS, with dependency injection (DI) we can delegate instantiation of dependencies to the runtime system, instead of doing it imperatively in our own code. For example, say we have a UserService defined as follows:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  constructor() {}
  public async getUserById(userId: string) {
    ...
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now, say we use the UserService as follows in the UserController class:

import { Controller } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  public async getUserById(userId: string) {
    return await this.userService.getUserById(userId);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this case, UserController is an enquirer asking for the UserService as one of its dependencies. The NestJS dependency injector will check for the requested dependency in a container, where it stores references to providers defined in the NestJS project.

The @Injectable() decorator that was used in the UserService definition marks the class as a provider that should be injectable by the NestJS dependency injection system, i.e., it should be managed by the container. When the TypeScript code is compiled by the compiler, this decorator emits metadata that the NestJS uses to manage dependency injection.

dependency injection Nestjs visualization

In NestJS, each module has its own injector that can access the container. When you declare a module, you have to specify the providers that should be available to the module, except in cases where the provider is a global provider.

For example, the UserModule is defined as follows:

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  providers: [UserService],
  controllers: [UserController],
  exports: [UserService],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

In a normal mode of execution, when an enquirer asks for a dependency, the injector checks the container to see if an object of that dependency has been cached previously. If so, that object is returned to the enquirer. Otherwise, NestJS instantiates a new object of the dependency, caches it, and then returns the object to the enquirer.

The declaration providers: [UserService] is actually a shorthand for the following:

providers: [
    {
      provide: UserService,
      useClass: UserService,
    },
]
Enter fullscreen mode Exit fullscreen mode

The value of provide is an injection token that is used to identify the provider when it’s been enquired.

How circular dependency issues arise

The NestJS DI system relies heavily on the metadata emitted by the TypeScript compiler, so when there is a circular reference between two modules or two providers, the compiler won’t be able to compile any of them without further help.

For example, say we have a FileService that we are using to manage files uploaded to our application, defined as follows:

import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { File } from './interfaces/file.interface';

@Injectable()
export class FileService {
  constructor(private readonly userService: UserService) {}
  public getById(pictureId: string): File {
    // not real implementation
    return {
      id: pictureId,
      url: 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50',
    };
  }

  public async getUserProfilePicture(userId: string): Promise<File> {
    const user = await this.userService.getUserById(userId);
    return this.getById(user.profilePictureId);
  }
}
Enter fullscreen mode Exit fullscreen mode

The service has a getUserProfilePicture method that gets the image file attached as a user profile picture. The FileService needs the UserService injected as a dependency to be able to fetch a user.

The UserService is also updated as follows:

import { Injectable } from '@nestjs/common';
import { FileService } from '../file-service/file.service';

@Injectable()
export class UserService {
  constructor(private readonly fileService: FileService) {}
  public async getUserById(userId: string) {
    // actual work of retrieving user
    return {
      id: userId,
      name: 'Sam',
      profilePictureId: 'kdkf43',
    };
  }

  public async addUserProfilePicture(userId: string, pictureId: string) {
    const picture = await this.fileService.getById(pictureId);
    // update user with the picture url
    return { id: userId, name: 'Sam', profilePictureId: picture.id };
  }
}
Enter fullscreen mode Exit fullscreen mode

We have a circular dependency in this case, as both UserService and FileService depend on each other (UserServiceFileServiceUserService). circular dependency visualization

With the circular reference in place, the code will fail to compile.

Avoiding circular dependencies by refactoring

The NestJS documentation advises that circular dependencies be avoided where possible.

Circular dependencies create tight couplings between the classes or modules involved, which means both classes or modules have to be recompiled every time either of them is changed. As I mentioned in a previous article, tight coupling is against the SOLID principles and we should endeavor to avoid it.

We can remove the circular dependency easily in this example. The circular reference we have can also be represented as the following:

UserService  FileService
and 
FileService  UserService
Enter fullscreen mode Exit fullscreen mode

To break the cycle, we can extract the common features from both services into a new service that depends on both services. In this case, we can have a ProfilePictureService that depends on both UserService and FileService.

The ProfilePictureService will have its own module defined as follows:

import { Module } from '@nestjs/common';
import { FileModule } from '../file-service/file.module';
import { UserModule } from '../user/user.module';
import { ProfilePictureService } from './profile-picture.service';
@Module({
  imports: [FileModule, UserModule],
  providers: [ProfilePictureService],
})
export class ProfilePictureModule {}
Enter fullscreen mode Exit fullscreen mode

Note that this module imports both the FileModule and the UserModule. Both imported modules have to export the services we want to use in ProfilePictureService.

The ProfilePictureService will be defined as follows:

import { Injectable } from '@nestjs/common';
import { File } from '../file-service/interfaces/file.interface';
import { FileService } from '../file-service/file.service';
import { UserService } from '../user/user.service';

@Injectable()
export class ProfilePictureService {
  constructor(
    private readonly fileService: FileService,
    private readonly userService: UserService,
  ) {}

  public async addUserProfilePicture(userId: string, pictureId: string) {
    const picture = await this.fileService.getById(pictureId);
    // update user with the picture url
    return { id: userId, name: 'Sam', profilePictureId: picture.id };
  }

  public async getUserProfilePicture(userId: string): Promise<File> {
    const user = await this.userService.getUserById(userId);
    return this.fileService.getById(user.profilePictureId);
  }
}
Enter fullscreen mode Exit fullscreen mode

ProfilePictureService requires both UserService and FileService as its dependencies, and contains methods performing the actions we were previously doing in both UserService and FileService.

The UserService doesn’t need to depend on FileService anymore, as you can see here:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  public async getUserById(userId: string) {
    // actual work of retrieving user
    return {
      id: userId,
      name: 'Sam',
      profilePictureId: 'kdkf43',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Similarly, FileService doesn’t need to know a thing about UserService:

import { Injectable } from '@nestjs/common';
import { File } from './interfaces/file.interface';

@Injectable()
export class FileService {
  public getById(pictureId: string): File {
    return {
      id: pictureId,
      url: 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50',
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The relationship between the three services can now be represented as follows: refactoring visualization As you can see from the diagram, there is no circular reference among the services.

Although this example on refactoring is about circular dependency between providers, one can use the same idea to avoid circular dependency among modules.

Working around circular dependencies with forward references

Ideally, circular dependencies should be avoided, but in cases where that’s not possible, Nest provides a way to work around them.

A forward reference allows Nest to reference classes that have not yet been defined by using the forwardRef() utility function. We have to use this function on both sides of the circular reference.

For example, we could have modified the UserService as follows:

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { FileService } from '../file-service/file.service';

@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => FileService))
    private readonly fileService: FileService,
  ) {}

  public async getUserById(userId: string) {
    ...
  }
  public async addFile(userId: string, pictureId: string) {
    const picture = await this.fileService.getById(pictureId);
    // update user with the picture url
    return { id: userId, name: 'Sam', profilePictureUrl: picture.url };
  }
}
Enter fullscreen mode Exit fullscreen mode

And then the FileService like so:

import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { File } from './interfaces/file.interface';

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

  public getById(pictureId: string): File {
    ...
  }

  public async getUserProfilePicture(userId: string): Promise<File> {
    const user = await this.userService.getUserById(userId);
    return this.getById(user.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

With this forward reference, the code will compile without errors.

Forward references for modules

The forwardRef() utility function can also be used to resolve circular dependencies between modules, but it must be used on both sides of the modules’ association. For example, one could do the following on one side of a circular module reference:

@Module({
  imports: [forwardRef(() => SecondCircularModule)],
})
export class FirstCircularModule {}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we learned what circular dependencies are, how dependency injection works in NestJS, and how issues of circular dependencies can arise.

We also learned how we can avoid circular dependencies in NestJS and why we should always try to avoid it. Hopefully, you now know how to work around it in case it can’t be avoided using forward references.

The code examples for this article are hosted here on GitHub; there are three branches in the repository named circular, fix/forward-referencing, and fix/refactoring. You can use the branches to navigate to the different stages of the project.


200’s only ✔️ Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Sign Up

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

Top comments (0)