Files
agent-skills/skills/nestjs-best-practices/rules/api-use-pipes.md
Jason Woltje 861b28b965 feat: Expand fleet to 23 skills across all domains
New skills (14):
- nestjs-best-practices: 40 priority-ranked rules (kadajett)
- fastapi: Pydantic v2, async SQLAlchemy, JWT auth (jezweb)
- architecture-patterns: Clean Architecture, Hexagonal, DDD (wshobson)
- python-performance-optimization: Profiling and optimization (wshobson)
- ai-sdk: Vercel AI SDK streaming and agent patterns (vercel)
- create-agent: Modular agent architecture with OpenRouter (openrouterteam)
- proactive-agent: WAL Protocol, compaction recovery, self-improvement (halthelobster)
- brand-guidelines: Brand identity enforcement (anthropics)
- ui-animation: Motion design with accessibility (mblode)
- marketing-ideas: 139 ideas across 14 categories (coreyhaines31)
- pricing-strategy: SaaS pricing and tier design (coreyhaines31)
- programmatic-seo: SEO at scale with playbooks (coreyhaines31)
- competitor-alternatives: Comparison page architecture (coreyhaines31)
- referral-program: Referral and affiliate programs (coreyhaines31)

README reorganized by domain: Code Quality, Frontend, Backend,
Auth, AI/Agent Building, Marketing, Design, Meta.

Mosaic Stack is not limited to coding — the Orchestrator serves
coding, business, design, marketing, writing, logistics, and analysis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:22:53 -06:00

5.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Pipes for Input Transformation MEDIUM Pipes ensure clean, validated data reaches your handlers 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):

// Manual type parsing in handlers
@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id') id: string): Promise<User> {
    // 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<User[]> {
    // 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<Product[]> {
  const priceNum = +price; // NaN if invalid, no error
  return this.productsService.findByPrice(priceNum);
}

Correct (use built-in and custom pipes):

// Use built-in pipes for common transformations
@Controller('users')
export class UsersController {
  @Get(':id')
  async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
    // 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<User[]> {
    // 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<User[]> {
    return this.usersService.findByStatus(status);
  }
}

// Custom pipe for business logic
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
  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<Report[]> {
  return this.reportsService.findBetween(from, to);
}

// Custom transformation pipes
@Injectable()
export class NormalizeEmailPipe implements PipeTransform<string, string> {
  transform(value: string): string {
    if (!value) return value;
    return value.trim().toLowerCase();
  }
}

// Parse comma-separated values
@Injectable()
export class ParseArrayPipe implements PipeTransform<string, string[]> {
  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<Product[]> {
  // ids is already an array, email is normalized
  return this.productsService.findByIds(ids);
}

// Sanitize HTML input
@Injectable()
export class SanitizeHtmlPipe implements PipeTransform<string, string> {
  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<Product[]> {
  // 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<Item> {
  return this.itemsService.findOne(id);
}

Reference: NestJS Pipes