We are building an API using Nest.js and MikroORM and we are having a debate on several things related to the service "layer" of Nest.js.
Let's suppose you have the following update method on a service.
This method does several things:
Firsts, asks to the ORM if the project exists. If thats not the case it returns a NotFoundError
Second, it checks if the client exists. If that's not the case, the service returns a NotFoundError.
Finally, proceeds with the update
async update(
id: number,
updateProjectDto: UpdateProjectDto,
): Promise<Projects> {
try {
const project = await this.projectRepository.findOneOrFail(id);
if (updateProjectDto.client_id) {
project.client = await this.clientService.findOne(
updateProjectDto.client_id,
);
}
this.projectRepository.assign(project, updateProjectDto);
await this.projectRepository.getEntityManager().flush();
return project;
} catch (error) {
this.logger.error('Error updating project', error);
throw error;
}
}
Here's our findOne in clients
async findOne(id: number) {
try {
return await this.clientsRepository.findOneOrFail({ id });
} catch (error) {
this.logger.error(`Error finding client`, error);
throw error;
}
}
Our questions are the following:
When should we wrap our business logic inside a try catch?
Should we use try catch for every method on our API or let the errors bubble up, as we are not doing anything in particular with them?
Should the service throw HTTPExceptions (taking into account its not the "proper" layer to do that). We think it's ok to throw them here for simplicity but who knows.
Thanks in advance!
If you want to follow Domain Driven Design and Clean Architecture principles, then in your domain layer you should not throw exceptions because they belong to the interface (or api) layer.
You can encapsulate the success or failure of the result using some libraries like Effect-TS or Oxyde.
In this approach, the contract of the repository will be like this:
export interface UserRepository {
createUser(data: UserProps): Promise<Option<User>>;
getUserByEmail(email: string): Promise<Option<User>>;
getUserById(id: string): Promise<Option<User>>;
checkActiveUserById(id: string): Promise<boolean>;
getAllUsers<T extends PaginatedQueryParams>(
params?: T,
): Promise<Collection<User>>;
}
And in the API controller you put the logic to return data or throw an exception:
@PublicApi()
@Post('/login')
async login(@Body() body: LoginBody): Promise<JwtUser> {
return this.jwtAuth.generateJwtUser(
getOrThrowWith(
await this.loginUseCase.execute(body),
() => new UnauthorizedException('Login Error!'),
),
);
}
If you want to deep dive into DDD and clean architecture i have created the following NestJS repository: https://github.com/andrea-acampora/nestjs-ddd-devops