DEV Community

Kazuki Matsuo
Kazuki Matsuo

Posted on • Originally published at kzmat.github.io

Module boundary and isolation of side effects using NestJS

Necessity of module

Software is unsure and subject to change, so it should be built boundry to resist change and hide the internal contents. The concept of isolation of side effects is not limited to NestJS, but providing a default DI and modularity by NestJS makes it easier to achieve, and I think NestJS is created with modularity in mind from following the quote.

Thus, for most applications, the resulting architecture will employ multiple modules, each encapsulating a closely related set of capabilities.

https://docs.nestjs.com/modules

In this article, I will write about the isolation of side effects using NestJS.

Directory structure

This is not the essential part of the article, but when we make an interface, a directory structure can sometimes be an issue. So, I write about what I think as of now.

Basically, I follow the structure of the official docs unless I have strong reasons to make a change. I think giving the discipline is the one pros to use framework. I know there is another way to make a directory presenter and so on.
However, as far as I understand it now, it is enough if important modules do not depend on unimportant modules. So we do not create these directories and follow the structure of the official documentation.

As of now, the closer the related modules are, the easier it is for me. Of course, the easiest way depends on the application scale, team, and so on, so this is just one example.

user
├── constants.ts
├── models
│   └── user.model.ts
├── repository
│   ├── user.repository.inmemory.ts
│   ├── user.repository.onrdb.ts
│   └── user.repository.ts
├── users.module.ts
└── users.service.ts
Enter fullscreen mode Exit fullscreen mode

Repository implementation

In this article, I write an example of abstraction of repository related to persistence. If these are not abstracted, the application always connects DB, which means it is hard to test, and it gives influences the caller when the kind of repository is changed.

  • user.repository.inmemory.ts
  • user.repository.onrdb.ts
// user.repository.ts
export interface UserRepository {
  findUser(id: string): Promise<User>;
}

// user.repository.inmemory.ts
@Injectable()
export class UserRepositoryInMemory implements UserRepository {
  async findUser(id: string): Promise<User> {

    const name = 'string';
    const imagePath = 'string';

    return {id, name, path};
  }
}

// user.repository.onrdb.ts
@Injectable()
export class UserRepositoryOnRDB implements UserRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findUser(id: string): Promise<User | undefined> {
    const user = await this.prisma.user.findUnique({ where: { id } });
    return user
  }
}
Enter fullscreen mode Exit fullscreen mode

Module implementation

Running the application with NODE_ENV === TEST as follows will isolate side effects and facilitate easy testing.

The reason why I use 'string' for INJECTION_TOKEN at provide is to avoid using 'abstract class.' An interface is used for type check and removed after transpiling, so we cannot use it at provide. On the other hand, "abstract classes" are possible because of transpiled to the 'Javascript class' but allow difference programming based on 'extend,' and it can increase complexity. So I use 'string' INJECTION_TOKEN.

It seems like the token is generated here, just in case.
https://github.com/nestjs/nest/blob/874344c60efddba0d8491f8bc6da0cd45f8ebdf7/packages/core/injector/injector.ts#L837-L839

// constants.ts
export const USER_REPOSITORY_INJECTION_TOKEN = 'USER_REPOSITORY_INJECTION_TOKEN';

// user.module.ts
@Module({
  providers: [
    UsersResolver,
    UsersService,
    {
      provide: USER_REPOSITORY_INJECTION_TOKEN,
      useClass:
        process.env.NODE_ENV === 'TEST'
          ? UserRepositoryInMemory
          : UserRepositoryOnRDB,
    },
  ],
  exports: [UsersService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

Service

When Using the repository, we can extract the repository instance from the DI container using REPOSITORY_INJECTION_TOKEN that is registered. The service class does not know what kind of repository is used.

@Injectable()
export class UsersService {
  constructor(
    @Inject(REPOSITORY_INJECTION_TOKEN)
    private readonly userRepository: UserRepository,
  ) {}
  async findUser(id: string): Promise<User> {
    return this.userRepository.findUser(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

As shown above, NestJS module system makes it easy to isolate modules. Of course, abstraction using DI is appliable not only to a repository but also to service and the other component. However, abstraction can increase the amount of implementation and may be a useless data refill to match the type for your application.

I think abstraction is not the absolute correct answer, but we have to decide where to be abstracted for each application and your team. On the other hand, DI is a powerful way that can isolate each module, and NestJS will provide it quickly.

Reference

Top comments (0)