Skip to main content

Pipes trong NestJS

Pipes là một công cụ mạnh mẽ để validatetransform 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:

  1. Transformation - Transform input data thành một dạng mong muốn (e.g., string → number)
  2. 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