260223:1415 20260223 nextJS & nestJS Best pratices
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s
This commit is contained in:
205
.agents/skills/nestjs-best-practices/rules/api-use-pipes.md
Normal file
205
.agents/skills/nestjs-best-practices/rules/api-use-pipes.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
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<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):**
|
||||
|
||||
```typescript
|
||||
// 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](https://docs.nestjs.com/pipes)
|
||||
Reference in New Issue
Block a user