Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/api-use-pipes.md
1---2title: Use Pipes for Input Transformation3impact: MEDIUM4impactDescription: Pipes ensure clean, validated data reaches your handlers5tags: api, pipes, validation, transformation6---78## Use Pipes for Input Transformation910Use 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.1112**Incorrect (manual type parsing in handlers):**1314```typescript15// Manual type parsing in handlers16@Controller('users')17export class UsersController {18@Get(':id')19async findOne(@Param('id') id: string): Promise<User> {20// Manual validation in every handler21const uuid = id.trim();22if (!isUUID(uuid)) {23throw new BadRequestException('Invalid UUID');24}25return this.usersService.findOne(uuid);26}2728@Get()29async findAll(30@Query('page') page: string,31@Query('limit') limit: string,32): Promise<User[]> {33// Manual parsing and defaults34const pageNum = parseInt(page) || 1;35const limitNum = parseInt(limit) || 10;36return this.usersService.findAll(pageNum, limitNum);37}38}3940// Type coercion without validation41@Get()42async search(@Query('price') price: string): Promise<Product[]> {43const priceNum = +price; // NaN if invalid, no error44return this.productsService.findByPrice(priceNum);45}46```4748**Correct (use built-in and custom pipes):**4950```typescript51// Use built-in pipes for common transformations52@Controller('users')53export class UsersController {54@Get(':id')55async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {56// id is guaranteed to be a valid UUID57return this.usersService.findOne(id);58}5960@Get()61async findAll(62@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,63@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,64): Promise<User[]> {65// Automatic defaults and type conversion66return this.usersService.findAll(page, limit);67}6869@Get('by-status/:status')70async findByStatus(71@Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,72): Promise<User[]> {73return this.usersService.findByStatus(status);74}75}7677// Custom pipe for business logic78@Injectable()79export class ParseDatePipe implements PipeTransform<string, Date> {80transform(value: string): Date {81const date = new Date(value);82if (isNaN(date.getTime())) {83throw new BadRequestException('Invalid date format');84}85return date;86}87}8889@Get('reports')90async getReports(91@Query('from', ParseDatePipe) from: Date,92@Query('to', ParseDatePipe) to: Date,93): Promise<Report[]> {94return this.reportsService.findBetween(from, to);95}9697// Custom transformation pipes98@Injectable()99export class NormalizeEmailPipe implements PipeTransform<string, string> {100transform(value: string): string {101if (!value) return value;102return value.trim().toLowerCase();103}104}105106// Parse comma-separated values107@Injectable()108export class ParseArrayPipe implements PipeTransform<string, string[]> {109transform(value: string): string[] {110if (!value) return [];111return value.split(',').map((v) => v.trim()).filter(Boolean);112}113}114115@Get('products')116async findProducts(117@Query('ids', ParseArrayPipe) ids: string[],118@Query('email', NormalizeEmailPipe) email: string,119): Promise<Product[]> {120// ids is already an array, email is normalized121return this.productsService.findByIds(ids);122}123124// Sanitize HTML input125@Injectable()126export class SanitizeHtmlPipe implements PipeTransform<string, string> {127transform(value: string): string {128if (!value) return value;129return sanitizeHtml(value, { allowedTags: [] });130}131}132133// Global validation pipe with transformation134app.useGlobalPipes(135new ValidationPipe({136whitelist: true, // Strip non-DTO properties137transform: true, // Auto-transform to DTO types138transformOptions: {139enableImplicitConversion: true, // Convert query strings to numbers140},141forbidNonWhitelisted: true, // Throw on extra properties142}),143);144145// DTO with transformation decorators146export class FindProductsDto {147@IsOptional()148@Type(() => Number)149@IsInt()150@Min(1)151page?: number = 1;152153@IsOptional()154@Type(() => Number)155@IsInt()156@Min(1)157@Max(100)158limit?: number = 10;159160@IsOptional()161@Transform(({ value }) => value?.toLowerCase())162@IsString()163search?: string;164165@IsOptional()166@Transform(({ value }) => value?.split(','))167@IsArray()168@IsString({ each: true })169categories?: string[];170}171172@Get()173async findAll(@Query() dto: FindProductsDto): Promise<Product[]> {174// dto is already transformed and validated175return this.productsService.findAll(dto);176}177178// Pipe error customization179@Injectable()180export class CustomParseIntPipe extends ParseIntPipe {181constructor() {182super({183exceptionFactory: (error) =>184new BadRequestException(`${error} must be a valid integer`),185});186}187}188189// Or use options on built-in pipes190@Get(':id')191async findOne(192@Param(193'id',194new ParseIntPipe({195errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,196exceptionFactory: () => new NotAcceptableException('ID must be numeric'),197}),198)199id: number,200): Promise<Item> {201return this.itemsService.findOne(id);202}203```204205Reference: [NestJS Pipes](https://docs.nestjs.com/pipes)206