Validate dữ liệu đầu vào là bước quan trọng để bảo vệ ứng dụng. NestJS tích hợp class-validator và class-transformer giúp validate DTO một cách khai báo, sạch sẽ và tái sử dụng được.
1. Cài đặt
npm install --save class-validator class-transformer
2. Bật Global ValidationPipe
Kích hoạt validate tự động cho toàn bộ app trong 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, // Tự động xóa field không có trong DTO
forbidNonWhitelisted: true, // Throw lỗi nếu có field thừa
transform: true, // Tự động chuyển đổi type (string → number...)
transformOptions: { enableImplicitConversion: true },
}),
);
await app.listen(3000);
}
bootstrap();
3. Các decorator phổ biến
Kiểu dữ liệu cơ bản
import {
IsString, IsNumber, IsEmail, IsBoolean, IsDate,
IsOptional, IsNotEmpty, IsArray, IsEnum,
Min, Max, MinLength, MaxLength, Length,
IsUrl, IsPhoneNumber, IsUUID,
} from 'class-validator';
import { Type } from 'class-transformer';
enum Role { ADMIN = 'admin', USER = 'user' }
export class CreateUserDto {
@IsNotEmpty({ message: 'Tên không được để trống' })
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail({}, { message: 'Email không hợp lệ' })
email: string;
@IsString()
@MinLength(8, { message: 'Mật khẩu tối thiểu 8 ký tự' })
password: string;
@IsOptional()
@IsNumber()
@Min(18, { message: 'Phải đủ 18 tuổi' })
@Max(120)
@Type(() => Number) // Chuyển string → number tự động
age?: number;
@IsEnum(Role, { message: 'Role không hợp lệ' })
role: Role;
@IsOptional()
@IsBoolean()
isActive?: boolean;
@IsOptional()
@IsUrl()
avatarUrl?: string;
@IsOptional()
@IsPhoneNumber('VN')
phone?: string;
}
Mảng và Object lồng nhau
import { ValidateNested, ArrayMinSize, ArrayMaxSize } from 'class-validator';
import { Type } from 'class-transformer';
export class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsOptional()
@IsString()
zipCode?: string;
}
export class CreateOrderDto {
@ValidateNested()
@Type(() => AddressDto) // BẮT BUỘC phải có @Type để validate nested object
shippingAddress: AddressDto;
@IsArray()
@ArrayMinSize(1, { message: 'Giỏ hàng phải có ít nhất 1 sản phẩm' })
@ArrayMaxSize(100)
@IsString({ each: true }) // Validate từng phần tử trong mảng
productIds: string[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items?: OrderItemDto[];
}
export class OrderItemDto {
@IsUUID()
productId: string;
@IsNumber()
@Min(1)
quantity: number;
}
4. Custom Validator
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
// Decorator tùy chỉnh: kiểm tra password xác nhận khớp
function IsPasswordConfirmed(property: string, validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isPasswordConfirmed',
target: object.constructor,
propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return value === relatedValue;
},
defaultMessage(args: ValidationArguments) {
return 'Mật khẩu xác nhận không khớp';
},
},
});
};
}
// Sử dụng
export class RegisterDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsString()
@IsPasswordConfirmed('password')
confirmPassword: string;
}
5. Transform với class-transformer
import { Transform, Exclude, Expose } from 'class-transformer';
export class UserDto {
@Expose()
id: string;
@Expose()
name: string;
@Expose()
@Transform(({ value }) => value.toLowerCase())
email: string;
@Exclude() // Field này sẽ KHÔNG được serialize
password: string;
@Expose()
@Transform(({ value }) => value ? 'active' : 'inactive')
status: string;
@Expose()
@Transform(({ value }) => new Date(value).toLocaleDateString('vi-VN'))
createdAt: string;
}
// Trong controller — chuyển entity sang DTO
import { plainToInstance } from 'class-transformer';
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(id);
return plainToInstance(UserDto, user, { excludeExtraneousValues: true });
}
6. Validate Query Params và Params
import { IsOptional, IsNumberString } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
search?: string;
}
// Controller
@Get()
findAll(@Query() query: PaginationDto) {
return this.service.findAll(query);
}
7. Response khi validate lỗi
Khi có lỗi, NestJS tự động trả về:
{
"statusCode": 400,
"message": [
"email phải là email hợp lệ",
"Mật khẩu tối thiểu 8 ký tự",
"age phải là số"
],
"error": "Bad Request"
}
8. Kết luận
class-validator + class-transformer + ValidationPipe là bộ ba không thể thiếu trong NestJS:
- Khai báo validation bằng decorator trực quan
whitelist: truetự động lọc field không mong muốn — bảo mật hơntransform: truetự động convert type — xử lý query params dễ dàng@ValidateNested+@Typeđể validate object lồng nhau- Custom decorator cho business rule phức tạp