In a nestjs APP, I am designing an API endpoint. While creating this endpoint I started having an issue in taking the right decision when it comes to validating incoming requests using a decent and correct pattern that doesn't create technical debt
I have a controller method that looks like this:
@Post()@AuthProtected()async create_medal( @Body() createMedal: CreateMedalDefDto, @Req() req, @Res() res: Response,) { ... }
The CreateMedalDefDto
contains a property named resource_id
and I want to put that property in the path param section of the uri to emphasize further that it's mandatory to the API users
Now separating this would make my Dto Obsolete, and also unusable because I need the resource_id
to validate the uniqueness of some fields and other logic constraints I am defining there.
I wrote this as a fix, it's just an interceptor that merges the properties from the request params to add them to the body or any other destination.
class MergeRouteParamsToBodyInterceptor implements NestInterceptor { constructor( protected source: 'params' | 'body' | 'query', protected dest: 'params' | 'body' | 'query', protected entryNames: string[], ) {} intercept( context: ExecutionContext, next: CallHandler<any>, ): Observable<any> | Promise<Observable<any>> { const req = context.switchToHttp().getRequest(); this.entryNames.forEach((param) => { if (req[this.source]?.[param]) { req.body[param] = req[this.dest][param]; } }); return next.handle(); }}export const ApiBodyParam = (dto: new () => any, paramProps: string[]) => { const strippedDto = OmitType(dto, paramProps); const params = paramProps.map((param) => ApiParam({ name: param, required: true }), ); return applyDecorators( ApiBody({ type: strippedDto }), ...params, UseInterceptors( new MergeRouteParamsToBodyInterceptor('params', 'body', paramProps), ), );};
Just as I started applying this decorator it occurred to me that this approach makes my code harder to understand and creates the fake idea that my properties are expected in the body when validated. Because the handler would look like this:
@ApiBodyParam(CreateMedalDefDto, ['resource_id'])@Post()@AuthProtected()async create_medal( @Body() createMedal: CreateMedalDefDto, @Req() req, @Res() res: Response,) {... }
This code directly implies that all the parameters are expected in the body, even if they are not. I would wrongfully read this in the future.
Therefore, this approach causes a lot of technical debt.
How can I solve this? Is there a better approach?
I want to merge the properties in the validation process of the request, but it shouldn't create this scenario where it makes my code impossible to understand and introduces necessary hidden logic.
The main issue is that the validation is bound to a single data source (body, params, or Query...) instead of being completely independent. Is this an issue from Nestjs?