This is a premium alert message you can set from Layout! Get Now!

How to avoid circular dependencies in NestJS

0

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) {
    ...
  }
  ...
}

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);
  }
}

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 {}

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,
    },
]

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);
  }
}

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 };
  }
}

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

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 it’s 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 {}

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);
  }
}

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',
    };
  }
}

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',
    };
  }
}

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 };
  }
}

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);
  }
}

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 {}

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.

The post How to avoid circular dependencies in NestJS appeared first on LogRocket Blog.



from LogRocket Blog https://ift.tt/mhw9Jla
via Read more

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Post a Comment

Search This Blog

To Top