Pipes trong NestJS
Pipes là một công cụ mạnh mẽ để validate và transform data. Một pipe là một class được trang trí bằng @Injectable() decorator, implement interface PipeTransform. Pipes được thực thi trước khi data tới controller methods.
Khái Niệm Pipe
Pipe có hai use cases chính:
- Transformation - Transform input data thành một dạng mong muốn (e.g., string → number)
- Validation - Validate input data và throw exception nếu invalid
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed: value must be a number');
}
return val;
}
}
Built-in Pipes
NestJS cung cấp sẵn các pipes hữu ích:
import {
ParseIntPipe,
ParseFloatPipe,
ParseBoolPipe,
ParseArrayPipe,
ParseUUIDPipe,
ParseEnumPipe,
ParseDatePipe,
ValidationPipe,
DefaultValuePipe,
} from '@nestjs/common';
// ParseIntPipe - Convert string to number
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return { id }; // id is number
}
// ParseBoolPipe - Convert string to boolean
@Get()
search(@Query('active', ParseBoolPipe) active: boolean) {
return { active }; // active is boolean
}
// ParseArrayPipe - Convert string to array
@Post('tags')
addTags(@Body('tags', ParseArrayPipe) tags: string[]) {
return { tags };
}
// ParseUUIDPipe - Validate UUID format
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return { id };
}
// ParseEnumPipe - Validate enum values
enum Role {
ADMIN = 'admin',
USER = 'user',
}
@Post()
create(@Body('role', new ParseEnumPipe(Role)) role: Role) {
return { role };
}
// DefaultValuePipe - Provide default value
@Get()
list(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {
return { page }; // page = 1 if not provided
}
Custom Pipes
1. Validation Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any) {
if (!value) {
throw new BadRequestException('Value is required');
}
if (typeof value !== 'string') {
throw new BadRequestException('Value must be a string');
}
return value.trim();
}
}
2. Transformation Pipe
import { PipeTransform, Injectable } from '@nestjs/common';
@Injectable()
export class ToUpperCasePipe implements PipeTransform {
transform(value: string): string {
return value.toUpperCase();
}
}
// Sử dụng
@Post()
create(@Body('name', ToUpperCasePipe) name: string) {
return { name }; // name sẽ UPPERCASE
}
3. Parse Int Pipe (Custom)
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(
`Validation failed: "${value}" is not a valid number`,
);
}
return val;
}
}
4. Validation Pipe với Default Value
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
interface ValidatePipeOptions {
defaultValue?: any;
required?: boolean;
}
@Injectable()
export class ValidatePipe implements PipeTransform {
constructor(private options: ValidatePipeOptions = {}) {}
transform(value: any) {
if (!value && this.options.defaultValue !== undefined) {
return this.options.defaultValue;
}
if (!value && this.options.required) {
throw new BadRequestException('This field is required');
}
return value;
}
}
Validation with class-validator
Setup
npm install class-validator class-transformer
DTOs with Decorators
import {
IsString,
IsEmail,
IsNumber,
IsOptional,
Min,
Max,
Length,
Matches,
IsEnum,
IsDate,
ValidateIf,
ValidateNested,
IsArray,
} from 'class-validator';
import { Type } from 'class-transformer';
enum UserRole {
ADMIN = 'admin',
USER = 'user',
MODERATOR = 'moderator',
}
class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
zipCode: string;
}
export class CreateUserDto {
@IsString()
@Length(3, 50)
name: string;
@IsEmail()
email: string;
@IsNumber()
@Min(18)
@Max(120)
age: number;
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
password: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
@IsOptional()
@IsDate()
@Type(() => Date)
dateOfBirth?: Date;
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ValidateIf((o) => o.role === UserRole.ADMIN)
@IsString()
adminCode?: string;
}
Global Validation Pipe
// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Loại bỏ properties không được định nghĩa
forbidNonWhitelisted: true, // Throw error nếu có unexpected properties
transform: true, // Auto-transform primitives to their types
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3000);
}
bootstrap();
Sử dụng với Controllers
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
// createUserDto được validate tự động
return this.usersService.create(createUserDto);
}
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.usersService.findAll(page, limit);
}
}
Pipes at Different Scopes
Method-level
@Controller('users')
export class UsersController {
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
}
Global-level
// main.ts
app.useGlobalPipes(new ValidationPipe());
app.useGlobalPipes(new TransformPipe());
Module-level (Provider)
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
Ví Dụ Thực Tế
1. Custom Validation Pipe
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
if (!metadata.type || metadata.type === 'custom') return value;
const object = plainToInstance(metadata.type, value);
const errors = await validate(object);
if (errors.length > 0) {
const formattedErrors = errors.reduce((acc, error) => {
acc[error.property] = Object.values(error.constraints || {});
return acc;
}, {});
throw new BadRequestException({
message: 'Validation failed',
errors: formattedErrors,
});
}
return object;
}
}
2. Parse Comma-Separated List Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseCSVPipe implements PipeTransform<string, string[]> {
transform(value: string): string[] {
if (!value) {
throw new BadRequestException('Value is required');
}
return value
.split(',')
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
}
// Sử dụng
@Get('search')
search(@Query('tags', ParseCSVPipe) tags: string[]) {
return { tags }; // tags = ['javascript', 'nodejs', 'nestjs']
}
3. Parse JSON Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseJSONPipe implements PipeTransform<string, object> {
transform(value: string): object {
try {
return JSON.parse(value);
} catch (error) {
throw new BadRequestException('Invalid JSON format');
}
}
}
// Sử dụng
@Post('data')
processData(@Body('metadata', ParseJSONPipe) metadata: object) {
return { metadata };
}
4. Sanitize HTML Pipe
import { PipeTransform, Injectable } from '@nestjs/common';
import * as sanitizeHtml from 'sanitize-html';
@Injectable()
export class SanitizeHtmlPipe implements PipeTransform<string, string> {
transform(value: string): string {
return sanitizeHtml(value, {
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
allowedAttributes: {
a: ['href'],
},
});
}
}
// Sử dụng
@Post()
create(@Body('content', SanitizeHtmlPipe) content: string) {
return { content }; // HTML sạch, an toàn
}
5. Trim String Pipe
import { PipeTransform, Injectable } from '@nestjs/common';
@Injectable()
export class TrimPipe implements PipeTransform<string, string> {
transform(value: string): string {
return typeof value === 'string' ? value.trim() : value;
}
}
// Sử dụng
@Post()
create(
@Body('name', TrimPipe) name: string,
@Body('email', TrimPipe) email: string,
) {
return { name, email };
}
6. Lowercase Pipe
import { PipeTransform, Injectable } from '@nestjs/common';
@Injectable()
export class LowercasePipe implements PipeTransform<string, string> {
transform(value: string): string {
return typeof value === 'string' ? value.toLowerCase() : value;
}
}
7. Custom Enum Validation Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
enum Status {
ACTIVE = 'active',
INACTIVE = 'inactive',
PENDING = 'pending',
}
@Injectable()
export class ValidateStatusPipe implements PipeTransform {
transform(value: string): Status {
const validStatuses = Object.values(Status);
if (!validStatuses.includes(value as Status)) {
throw new BadRequestException(
`Invalid status. Must be one of: ${validStatuses.join(', ')}`,
);
}
return value as Status;
}
}
// Sử dụng
@Patch(':id/status')
updateStatus(
@Param('id', ParseIntPipe) id: number,
@Body('status', ValidateStatusPipe) status: Status,
) {
return { id, status };
}
8. File Upload Validation Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
interface FileValidationOptions {
maxSize?: number; // bytes
allowedMimes?: string[];
}
@Injectable()
export class FileValidationPipe implements PipeTransform {
constructor(private options: FileValidationOptions = {}) {}
transform(file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('File is required');
}
const { maxSize = 5 * 1024 * 1024, allowedMimes = ['image/jpeg', 'image/png'] } = this.options;
if (file.size > maxSize) {
throw new BadRequestException(
`File size must be less than ${maxSize / 1024 / 1024}MB`,
);
}
if (!allowedMimes.includes(file.mimetype)) {
throw new BadRequestException(
`File type must be one of: ${allowedMimes.join(', ')}`,
);
}
return file;
}
}
// Sử dụng
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile(FileValidationPipe) file: Express.Multer.File) {
return { filename: file.filename };
}
9. Date Parsing Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@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: ${value}`);
}
return date;
}
}
// Sử dụng
@Get('range')
getByDateRange(
@Query('from', ParseDatePipe) from: Date,
@Query('to', ParseDatePipe) to: Date,
) {
return { from, to };
}
Validation Decorators
Built-in Decorators
// String validators
@IsString()
@IsEmail()
@IsUrl()
@IsIP()
@IsPhoneNumber()
@Length(min, max)
@MinLength(length)
@MaxLength(length)
@Matches(regex)
// Number validators
@IsNumber()
@IsInt()
@Min(value)
@Max(value)
@IsPositive()
@IsNegative()
// Boolean validators
@IsBoolean()
// Date validators
@IsDate()
@IsFuture()
@IsPast()
// Array validators
@IsArray()
@ArrayMinSize(min)
@ArrayMaxSize(max)
@ArrayContains(values)
@ArrayNotContains(values)
// Common validators
@IsOptional()
@IsEmpty()
@IsDefined()
@IsNotEmpty()
@IsEnum(enum)
@ValidateIf(condition)
@ValidateNested()
@Type(() => SomeClass)
Best Practices
1. Reusable DTOs
// base.dto.ts
export class BaseDto {
@IsOptional()
@IsDate()
@Type(() => Date)
createdAt?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
updatedAt?: Date;
}
// create-user.dto.ts
export class CreateUserDto extends BaseDto {
@IsString()
@Length(3, 50)
name: string;
@IsEmail()
email: string;
}
// update-user.dto.ts
export class UpdateUserDto extends PartialType(CreateUserDto) {}
2. Nested Validation
class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
}
class CreateUserDto {
@IsString()
name: string;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
3. Conditional Validation
enum UserType {
INDIVIDUAL = 'individual',
BUSINESS = 'business',
}
export class CreateUserDto {
@IsEnum(UserType)
type: UserType;
@ValidateIf((o) => o.type === UserType.BUSINESS)
@IsString()
companyName?: string;
@ValidateIf((o) => o.type === UserType.INDIVIDUAL)
@IsDate()
dateOfBirth?: Date;
}
4. Custom Validation Decorators
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';
// Decorator kiểm tra email duy nhất
export function IsUniqueEmail(validationOptions?: ValidationOptions) {
return function (target: Object, propertyName: string) {
registerDecorator({
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
async validate(value: any, args: ValidationArguments) {
const userRepository = // Get repository
const user = await userRepository.findOne({ email: value });
return !user;
},
defaultMessage(args: ValidationArguments) {
return `Email ${args.value} already exists`;
},
},
});
};
}
// Sử dụng
export class CreateUserDto {
@IsEmail()
@IsUniqueEmail()
email: string;
}
5. Validation Groups
import { ValidationPipe } from '@nestjs/common';
// Riêng biệt validation cho create và update
@Post()
create(
@Body(new ValidationPipe({ groups: ['create'] }))
createUserDto: CreateUserDto,
) {
return this.usersService.create(createUserDto);
}
@Put(':id')
update(
@Body(new ValidationPipe({ groups: ['update'] }))
updateUserDto: UpdateUserDto,
) {
return this.usersService.update(updateUserDto);
}
Complete Example
// dtos/create-user.dto.ts
import { IsString, IsEmail, IsNumber, Min, Max, Length, ValidateNested, Type } from 'class-validator';
class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
zipCode: string;
}
export class CreateUserDto {
@IsString()
@Length(3, 50, { message: 'Name must be between 3 and 50 characters' })
name: string;
@IsEmail({}, { message: 'Invalid email format' })
email: string;
@IsNumber()
@Min(18, { message: 'Age must be at least 18' })
@Max(120, { message: 'Age must be at most 120' })
age: number;
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
// pipes/custom-validation.pipe.ts
import { PipeTransform, Injectable, BadRequestException, ArgumentMetadata } from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
export class CustomValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
if (!metadata.type || metadata.type === 'custom') return value;
const object = plainToInstance(metadata.type, value);
const errors = await validate(object);
if (errors.length > 0) {
const formattedErrors = errors.reduce((acc, error) => {
acc[error.property] = Object.values(error.constraints || {});
return acc;
}, {});
throw new BadRequestException({
statusCode: 400,
message: 'Validation failed',
errors: formattedErrors,
timestamp: new Date().toISOString(),
});
}
return object;
}
}
// users.controller.ts
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Post()
create(@Body(CustomValidationPipe) createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.usersService.findAll(page, limit);
}
}
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new CustomValidationPipe(),
);
await app.listen(3000);
}
bootstrap();
Kết Luận
Pipes là công cụ quan trọng để:
- Validate input data trước khi tới logic xử lý
- Transform data thành dạng mong muốn
- Đảm bảo type safety
- Tạo consistent error responses
- Giảm boilerplate code trong controllers
Sử dụng pipes đúng cách giúp bạn:
- Xây dựng ứng dụng robust và secure
- Validate data một cách centralized
- Cải thiện code quality
- Giảm bugs liên quan đến invalid data