A week ago, I was working on an API, and it was a dynamic one or in other words a one for four. The endpoint: http://localhost:5000/api/v2/item/{itemId}/readings. Depending on the item ID, the item changes and query params passed also change. And in order to validate these changing query params I required a dynamically validating pipe.
The Internet didn't help (maybe I didn't search with the right keywords). Tried ChatGPT, Gemini, and Bing, which had Copilot. TBH, Gemini was the dumbest, Bing the smartest but with a hiccup, and surprisingly Chatgpt provided the nearest solution. After tweaking some lines of code, I got what I wanted and thought to share it here. Please don't hesitate to point out the mistakes in the comments.
Starting with the Pipe I created
@Injectable()
export class CustomDTOValidationPipe implements PipeTransform<any> {
transform(value: any, { metatype }: ArgumentMetadata): Promise<any> {
if (!metatype || !this.isDtoType(metatype)) {
return value;
}
const dtoClass = moduleDTORegistry[value.moduleType];
if (!dtoClass) {
throw new BadRequestException(AppConstants.INVALID_MODULE_TYPE);
}
//to make the query object an instance of the DTO passed.
const dtoInstance = plainToClass(DynamicModulesParamsDto, value);
const errors = validateSync(dtoInstance, {
skipMissingProperties: false,
validationError: { target: true },
});
private isDto(metatype: any): boolean {
return metatype && metatype.prototype instanceof Object;
}
if (errors.length > 0) {
throw new BadRequestException(this.extractErrorMessages(errors));
}
return value;
}
private isDtoType(metatype: any): boolean {
return metatype && metatype.prototype instanceof Object;
}
/* {
"target": {
"dateProcessed": "12-03-202",
"timezone": "12:34:34323",
"timestamp": "213",
"state": "213",
"plate": "123",
"type": "213",
"minConfidence": "213",
"maxConfidence": "213",
"moduleType": "hazmat"
},
"value": "12-03-202",
"property": "dayProcessed",
"children": [],
"constraints": { "isDate": "dateProcessed must be in the dd:mm:yyyy format" }
},
given above is the structure of error we get from
validateSync method. Therefore, we need to take only the constraints to return it in the response.*/
private extractErrorMessages(errors: any[]): string[] {
return errors.map((error) => {
const errorConstraints = error.constraints;
return errorConstraints[Object.keys(errorConstraints)[0]];
});
}
}
For the sake of reusability, I have created a mapping for the 'moduleType' and its respective DTO.
export const moduleDTOMapping = {
person: PersonQueryParamsDto,
}
Since this pipe is in place, whenever a new kind of item comes in this endpoint you just have to add its name as the property and DTO in the above mapping.
When a client hits the endpoint
pipe checks the 'moduleType' and validates the query params with its corresponding DTO.
dynamic-items.controller.ts
@ApiQuery({
name: 'moduleType',
type: 'string',
enum: ModuleType,
required: true,
})
@Get(':itemId/readings')
getReadings(
@Req() req: Request,
@Param('item', new IdValidationPipe()) id: number,
@Query(new DynamicDTOValidationPipe()) queryParams: DynamicModulesParamsDto,
) {
//controller logic goes here
}
To recapitulate, This article creates a pipe for validating multiple DTOs based on a query param. For example, for type = person, a person's DTO will be validated, and if the type is a dog, then the dog's DTO is validated. Given that the DTO and type mapping are declared somewhere.
Top comments (0)