Module boundary and isolation of side effects using NestJS
Kazuki Matsuo
Posted on March 7, 2022
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
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
}
}
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 {}
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);
}
}
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
Posted on March 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.