--- title: Use Pipes for Input Transformation impact: MEDIUM impactDescription: Pipes ensure clean, validated data reaches your handlers tags: api, pipes, validation, transformation --- ## Use Pipes for Input Transformation Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers. **Incorrect (manual type parsing in handlers):** ```typescript // Manual type parsing in handlers @Controller('users') export class UsersController { @Get(':id') async findOne(@Param('id') id: string): Promise { // Manual validation in every handler const uuid = id.trim(); if (!isUUID(uuid)) { throw new BadRequestException('Invalid UUID'); } return this.usersService.findOne(uuid); } @Get() async findAll( @Query('page') page: string, @Query('limit') limit: string, ): Promise { // Manual parsing and defaults const pageNum = parseInt(page) || 1; const limitNum = parseInt(limit) || 10; return this.usersService.findAll(pageNum, limitNum); } } // Type coercion without validation @Get() async search(@Query('price') price: string): Promise { const priceNum = +price; // NaN if invalid, no error return this.productsService.findByPrice(priceNum); } ``` **Correct (use built-in and custom pipes):** ```typescript // Use built-in pipes for common transformations @Controller('users') export class UsersController { @Get(':id') async findOne(@Param('id', ParseUUIDPipe) id: string): Promise { // id is guaranteed to be a valid UUID return this.usersService.findOne(id); } @Get() async findAll( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number, ): Promise { // Automatic defaults and type conversion return this.usersService.findAll(page, limit); } @Get('by-status/:status') async findByStatus( @Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus, ): Promise { return this.usersService.findByStatus(status); } } // Custom pipe for business logic @Injectable() export class ParseDatePipe implements PipeTransform { transform(value: string): Date { const date = new Date(value); if (isNaN(date.getTime())) { throw new BadRequestException('Invalid date format'); } return date; } } @Get('reports') async getReports( @Query('from', ParseDatePipe) from: Date, @Query('to', ParseDatePipe) to: Date, ): Promise { return this.reportsService.findBetween(from, to); } // Custom transformation pipes @Injectable() export class NormalizeEmailPipe implements PipeTransform { transform(value: string): string { if (!value) return value; return value.trim().toLowerCase(); } } // Parse comma-separated values @Injectable() export class ParseArrayPipe implements PipeTransform { transform(value: string): string[] { if (!value) return []; return value.split(',').map((v) => v.trim()).filter(Boolean); } } @Get('products') async findProducts( @Query('ids', ParseArrayPipe) ids: string[], @Query('email', NormalizeEmailPipe) email: string, ): Promise { // ids is already an array, email is normalized return this.productsService.findByIds(ids); } // Sanitize HTML input @Injectable() export class SanitizeHtmlPipe implements PipeTransform { transform(value: string): string { if (!value) return value; return sanitizeHtml(value, { allowedTags: [] }); } } // Global validation pipe with transformation app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip non-DTO properties transform: true, // Auto-transform to DTO types transformOptions: { enableImplicitConversion: true, // Convert query strings to numbers }, forbidNonWhitelisted: true, // Throw on extra properties }), ); // DTO with transformation decorators export class FindProductsDto { @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number = 1; @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number = 10; @IsOptional() @Transform(({ value }) => value?.toLowerCase()) @IsString() search?: string; @IsOptional() @Transform(({ value }) => value?.split(',')) @IsArray() @IsString({ each: true }) categories?: string[]; } @Get() async findAll(@Query() dto: FindProductsDto): Promise { // dto is already transformed and validated return this.productsService.findAll(dto); } // Pipe error customization @Injectable() export class CustomParseIntPipe extends ParseIntPipe { constructor() { super({ exceptionFactory: (error) => new BadRequestException(`${error} must be a valid integer`), }); } } // Or use options on built-in pipes @Get(':id') async findOne( @Param( 'id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, exceptionFactory: () => new NotAcceptableException('ID must be numeric'), }), ) id: number, ): Promise { return this.itemsService.findOne(id); } ``` Reference: [NestJS Pipes](https://docs.nestjs.com/pipes)