Best practice là kiểm tra tính đúng đắn của mọi dữ liệu gửi vào một ứng dụng web. Để tự động kiểm tra các request đến, Nest cung cấp sẵn nhiều pipe:
ValidationPipeStandardSchemaValidationPipeParseIntPipeParseBoolPipeParseArrayPipeParseUUIDPipe
ValidationPipe tận dụng package mạnh mẽ class-validator và các decorator kiểm tra dữ liệu khai báo của nó. ValidationPipe cung cấp một cách tiếp cận tiện lợi để áp đặt các quy tắc validation cho mọi payload từ client, với các quy tắc cụ thể được khai báo bằng annotation đơn giản trong khai báo class/DTO cục bộ ở mỗi module.
Nest cũng bao gồm sẵn StandardSchemaValidationPipe cho các dự án ưa thích các thư viện validation theo hướng schema-first như Zod, Valibot, ArkType, và các công cụ tương thích Standard Schema khác.
Tổng quan
Trong chương Pipe, chúng ta đã đi qua quá trình xây dựng các pipe đơn giản và gắn (bind) chúng vào controller, method hoặc vào toàn bộ app để minh họa cách hoạt động của quá trình này. Hãy chắc chắn xem lại chương đó để hiểu rõ nhất các chủ đề trong chương này. Ở đây, chúng ta sẽ tập trung vào các trường hợp sử dụng thực tế khác nhau của ValidationPipe, và chỉ ra cách dùng một số tính năng tùy chỉnh nâng cao của nó.
Sử dụng ValidationPipe có sẵn
Để bắt đầu sử dụng, trước tiên ta cài đặt dependency cần thiết.
$ npm i --save class-validator class-transformer
Gợi ý ValidationPipe được export từ package @nestjs/common.
Sử dụng StandardSchemaValidationPipe có sẵn
Nếu dự án của bạn đã định nghĩa request schema bằng một thư viện tương thích Standard Schema, bạn có thể gắn chúng trực tiếp vào route decorator và kiểm tra dữ liệu bằng StandardSchemaValidationPipe.
import { z } from 'zod';
import { Body, Controller, Param, Post, Get, StandardSchemaValidationPipe } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Post()
create(@Body({ schema: createUserSchema }) body: CreateUserDto) {
return body;
}
@Get(':id')
findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {
return { id };
}
}
Đăng ký pipe này ở phạm vi global:
app.useGlobalPipes(new StandardSchemaValidationPipe());
Mặc định, pipe trả về giá trị do schema tạo ra. Điều này hữu ích khi schema của bạn thực hiện coercion (ép kiểu) hoặc transformation (chuyển đổi dữ liệu).
app.useGlobalPipes(
new StandardSchemaValidationPipe({
transform: true,
}),
);
Nếu bạn cũng muốn nó kiểm tra dữ liệu do các custom parameter decorator tạo bằng createParamDecorator() sinh ra, hãy bật validateCustomDecorators.
app.useGlobalPipes(
new StandardSchemaValidationPipe({
validateCustomDecorators: true,
}),
);
Dùng cách tiếp cận này khi schema của bạn đã tồn tại độc lập bên ngoài các DTO dạng class. Nếu dự án của bạn dựa vào decorator của class-validator, ValidationPipe vẫn là lựa chọn đúng đắn.
Gợi ý Cùng một bộ schema có thể dùng để tạo tài liệu OpenAPI. Xem Standard Schema (Zod, Valibot) trong chương OpenAPI.
Ghi chú người dịch Đây là điểm khác biệt lớn nhất so với validation kiểu Laravel ($request->validate([...]) bằng rule string) hay Express (thường dùng middleware express-validator/joi độc lập với model): Nest cho bạn hai trường phái song song trong cùng framework - decorator gắn trên class DTO (class-validator, giống annotation validation của Java/Spring) và schema-first độc lập (zod, giống cách Go hay dùng struct tag + validator, hoặc thư viện ozzo-validation). Bạn chọn một trong hai theo phong cách team, không nên trộn lẫn tùy tiện trong cùng một route để tránh double-validate.
Vì pipe này dùng thư viện class-validator và class-transformer, nên có rất nhiều tùy chọn khả dụng. Bạn cấu hình các thiết lập này thông qua một object cấu hình truyền vào pipe. Dưới đây là các tùy chọn có sẵn:
export interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
disableErrorMessages?: boolean;
exceptionFactory?: (errors: ValidationError[]) => any;
errorFormat?: 'list' | 'grouped';
}
Ngoài các tùy chọn này, tất cả tùy chọn của class-validator (kế thừa từ interface ValidatorOptions) cũng khả dụng:
| Option | Type | Description |
|---|---|---|
enableDebugMessages |
boolean |
Nếu đặt là true, validator sẽ in thêm các cảnh báo ra console khi có gì đó không đúng. |
skipUndefinedProperties |
boolean |
Nếu đặt là true thì validator sẽ bỏ qua việc kiểm tra mọi property có giá trị undefined trong object đang được kiểm tra. |
skipNullProperties |
boolean |
Nếu đặt là true thì validator sẽ bỏ qua việc kiểm tra mọi property có giá trị null trong object đang được kiểm tra. |
skipMissingProperties |
boolean |
Nếu đặt là true thì validator sẽ bỏ qua việc kiểm tra mọi property có giá trị null hoặc undefined trong object đang được kiểm tra. |
whitelist |
boolean |
Nếu đặt là true, validator sẽ loại bỏ khỏi object trả về (đã kiểm tra) mọi property không sử dụng decorator validation nào. |
forbidNonWhitelisted |
boolean |
Nếu đặt là true, thay vì loại bỏ các property không nằm trong whitelist, validator sẽ ném ra một exception. |
forbidUnknownValues |
boolean |
Nếu đặt là true, các nỗ lực kiểm tra object không xác định sẽ thất bại ngay lập tức. |
disableErrorMessages |
boolean |
Nếu đặt là true, các lỗi validation sẽ không được trả về cho client. |
errorHttpStatusCode |
number |
Thiết lập này cho phép bạn chỉ định loại exception nào sẽ được dùng khi có lỗi. Mặc định nó ném ra BadRequestException. |
exceptionFactory |
Function |
Nhận vào một mảng các lỗi validation và trả về một exception object để ném ra. |
groups |
string[] |
Các group được dùng trong khi kiểm tra object. |
always |
boolean |
Đặt giá trị mặc định cho tùy chọn always của decorator. Giá trị mặc định có thể bị ghi đè trong tùy chọn của decorator. |
strictGroups |
boolean |
Nếu groups không được truyền vào hoặc rỗng, bỏ qua các decorator có ít nhất một group. |
dismissDefaultMessages |
boolean |
Nếu đặt là true, validation sẽ không dùng thông báo mặc định. Thông báo lỗi sẽ luôn là undefined nếu nó không được thiết lập tường minh. |
validationError.target |
boolean |
Chỉ định target có được hiển thị trong ValidationError hay không. |
validationError.value |
boolean |
Chỉ định giá trị đã kiểm tra có được hiển thị trong ValidationError hay không. |
stopAtFirstError |
boolean |
Khi đặt là true, việc kiểm tra của property đó sẽ dừng lại ngay sau khi gặp lỗi đầu tiên. Mặc định là false. |
errorFormat |
'list' | 'grouped' |
Chỉ định định dạng của các thông báo lỗi validation. 'list' (mặc định) trả về một mảng các chuỗi thông báo lỗi. 'grouped' trả về một object với các property path làm key và mảng các thông báo lỗi gốc (không sửa đổi) làm value, giữ nguyên các thông báo validation tùy chỉnh mà không thêm tiền tố parent path. |
Lưu ý Tìm thêm thông tin về package class-validator trong repository của nó.
Tự động kiểm tra dữ liệu (Auto-validation)
Chúng ta sẽ bắt đầu bằng cách gắn ValidationPipe ở phạm vi application, nhờ đó đảm bảo mọi endpoint đều được bảo vệ khỏi việc nhận dữ liệu sai.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
Để kiểm tra pipe của chúng ta, hãy tạo một endpoint cơ bản.
@Post()
create(@Body() createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
Gợi ý Vì TypeScript không lưu metadata về generic hay interface, khi bạn dùng chúng trong DTO, ValidationPipe có thể không kiểm tra dữ liệu đến một cách đúng đắn. Vì lý do này, hãy cân nhắc dùng các class cụ thể (concrete classes) trong DTO của bạn.
Gợi ý Khi import DTO của bạn, bạn không thể dùng type-only import vì nó sẽ bị xóa lúc runtime, tức là hãy nhớ dùng import { CreateUserDto } thay vì import type { CreateUserDto }.
Bây giờ chúng ta có thể thêm một vài quy tắc validation vào CreateUserDto. Ta làm điều này bằng các decorator do package class-validator cung cấp, được mô tả chi tiết ở đây. Theo cách này, bất kỳ route nào dùng CreateUserDto sẽ tự động áp dụng các quy tắc validation này.
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsNotEmpty()
password: string;
}
Với các quy tắc này, nếu một request gọi tới endpoint của chúng ta với property email không hợp lệ trong request body, ứng dụng sẽ tự động phản hồi với mã 400 Bad Request, cùng với response body sau:
{
"statusCode": 400,
"error": "Bad Request",
"message": ["email must be an email"]
}
Ngoài việc kiểm tra request body, ValidationPipe cũng có thể dùng với các property khác của request object. Giả sử chúng ta muốn nhận :id trong đường dẫn endpoint. Để đảm bảo chỉ số mới được chấp nhận cho tham số request này, ta có thể dùng cấu trúc sau:
@Get(':id')
findOne(@Param() params: FindOneParams) {
return 'This action returns a user';
}
FindOneParams, giống như một DTO, đơn giản là một class định nghĩa các quy tắc validation bằng class-validator. Nó sẽ trông như sau:
import { IsNumberString } from 'class-validator';
export class FindOneParams {
@IsNumberString()
id: string;
}
Ghi chú người dịch Điểm cần lưu ý: ValidationPipe chỉ hoạt động dựa trên metadata do TypeScript sinh ra khi compile, không phải kiểm tra kiểu lúc runtime thật sự - đây là lý do vì sao generic/interface “vô hình” với nó (bị erase sau khi compile sang JS), còn class thì vẫn tồn tại như một hàm constructor thực sự lúc runtime nên class-validator mới soi được. Cạm bẫy phổ biến khi vibe-code: khai dto: Partial<CreateUserDto> hoặc dùng interface cho DTO tưởng chừng tương đương class nhưng validation sẽ lặng lẽ không chạy, không có lỗi nào báo cả.
Tắt thông báo lỗi chi tiết (Disable detailed errors)
Thông báo lỗi có thể hữu ích để giải thích điều gì sai trong một request. Tuy nhiên, một số môi trường production lại muốn tắt các lỗi chi tiết. Làm điều này bằng cách truyền một object tùy chọn vào ValidationPipe:
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
}),
);
Kết quả là, các thông báo lỗi chi tiết sẽ không hiển thị trong response body.
Loại bỏ property thừa (Stripping properties)
ValidationPipe của chúng ta cũng có thể lọc bỏ các property mà method handler không nên nhận. Trong trường hợp này, ta có thể whitelist các property được chấp nhận, và bất kỳ property nào không nằm trong whitelist sẽ tự động bị loại bỏ khỏi object kết quả. Ví dụ, nếu handler của chúng ta chỉ mong đợi property email và password, nhưng một request lại gửi thêm property age, property này có thể tự động bị loại bỏ khỏi DTO kết quả. Để bật hành vi này, đặt whitelist thành true.
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
Khi đặt là true, việc này sẽ tự động loại bỏ các property không nằm trong whitelist (những property không có decorator validation nào trong class validation).
Ngoài ra, bạn có thể chặn request được xử lý tiếp khi có property không nằm trong whitelist, và trả về một response lỗi cho người dùng. Để bật điều này, đặt tùy chọn forbidNonWhitelisted thành true, kết hợp với việc đặt whitelist thành true.
Chuyển đổi payload object (Transform payload objects)
Payload đến qua mạng là các object JavaScript thuần. ValidationPipe có thể tự động chuyển đổi payload thành object có kiểu dữ liệu đúng theo class DTO. Để bật tự động chuyển đổi, đặt transform thành true. Việc này có thể thực hiện ở cấp method:
@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
Để bật hành vi này ở phạm vi global, đặt tùy chọn trên một pipe global:
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
Với tùy chọn tự động chuyển đổi được bật, ValidationPipe cũng sẽ thực hiện chuyển đổi các kiểu dữ liệu nguyên thủy (primitive types). Trong ví dụ dưới đây, phương thức findOne() nhận một tham số đại diện cho path parameter id được trích xuất:
@Get(':id')
findOne(@Param('id') id: number) {
console.log(typeof id === 'number'); // true
return 'This action returns a user';
}
Mặc định, mọi path parameter và query parameter đến qua mạng đều ở dạng string. Trong ví dụ trên, chúng ta đã chỉ định kiểu của id là number (trong chữ ký của phương thức). Vì vậy, ValidationPipe sẽ cố gắng tự động chuyển đổi một chuỗi định danh thành số.
Chuyển đổi tường minh (Explicit conversion)
Ở phần trên, chúng ta đã chỉ ra cách ValidationPipe có thể ngầm chuyển đổi query và path parameter dựa trên kiểu dữ liệu mong đợi. Tuy nhiên, tính năng này yêu cầu phải bật tự động chuyển đổi (auto-transformation).
Ngoài ra (khi tắt tự động chuyển đổi), bạn có thể ép kiểu tường minh bằng ParseIntPipe hoặc ParseBoolPipe (lưu ý ParseStringPipe là không cần thiết vì, như đã nói ở trên, mọi path parameter và query parameter mặc định đến qua mạng đều ở dạng string).
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('sort', ParseBoolPipe) sort: boolean,
) {
console.log(typeof id === 'number'); // true
console.log(typeof sort === 'boolean'); // true
return 'This action returns a user';
}
Gợi ý ParseIntPipe và ParseBoolPipe được export từ package @nestjs/common.
Mapped type
Khi xây dựng các tính năng như CRUD (Create/Read/Update/Delete) việc dựng ra các biến thể trên một entity type cơ sở thường rất hữu ích. Nest cung cấp một số hàm tiện ích thực hiện chuyển đổi kiểu để việc này thuận tiện hơn.
Cảnh báo Nếu ứng dụng của bạn dùng package
@nestjs/swagger, xem chương này để biết thêm thông tin về Mapped Types. Tương tự, nếu bạn dùng package@nestjs/graphqlxem chương này. Cả hai package đều phụ thuộc nặng vào type nên chúng yêu cầu một import khác để sử dụng. Vì vậy, nếu bạn dùng@nestjs/mapped-types(thay vì package phù hợp, hoặc@nestjs/swaggerhoặc@nestjs/graphqltùy loại ứng dụng của bạn), bạn có thể gặp phải nhiều tác dụng phụ không được ghi trong tài liệu.
Khi xây dựng các kiểu validation đầu vào (cũng gọi là DTO), việc dựng ra các biến thể create và update trên cùng một type thường rất hữu ích. Ví dụ, biến thể create có thể yêu cầu tất cả field, trong khi biến thể update có thể làm cho tất cả field trở thành tùy chọn.
Nest cung cấp hàm tiện ích PartialType() để việc này dễ dàng hơn và giảm thiểu code khung.
Hàm PartialType() trả về một type (class) với tất cả property của type đầu vào được đặt thành tùy chọn. Ví dụ, giả sử chúng ta có type create như sau:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Mặc định, tất cả các field này đều bắt buộc. Để tạo một type có cùng các field, nhưng mỗi field đều tùy chọn, dùng PartialType() truyền vào tham chiếu class (CreateCatDto) làm tham số:
export class UpdateCatDto extends PartialType(CreateCatDto) {}
Gợi ý Hàm PartialType() được import từ package @nestjs/mapped-types.
Hàm PickType() dựng một type (class) mới bằng cách chọn một tập property từ một type đầu vào. Ví dụ, giả sử ta bắt đầu với một type như:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Ta có thể chọn một tập property từ class này bằng hàm tiện ích PickType():
export class UpdateCatAgeDto extends PickType(CreateCatDto, ['age'] as const) {}
Gợi ý Hàm PickType() được import từ package @nestjs/mapped-types.
Hàm OmitType() dựng một type bằng cách chọn tất cả property từ một type đầu vào rồi loại bỏ một tập key cụ thể. Ví dụ, giả sử ta bắt đầu với một type như:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
Ta có thể tạo một type dẫn xuất có mọi property trừ name như dưới đây. Trong cấu trúc này, tham số thứ hai của OmitType là một mảng tên property.
export class UpdateCatDto extends OmitType(CreateCatDto, ['name'] as const) {}
Gợi ý Hàm OmitType() được import từ package @nestjs/mapped-types.
Hàm IntersectionType() kết hợp hai type thành một type (class) mới. Ví dụ, giả sử ta bắt đầu với hai type như:
export class CreateCatDto {
name: string;
breed: string;
}
export class AdditionalCatInfo {
color: string;
}
Ta có thể tạo một type mới kết hợp tất cả property trong cả hai type.
export class UpdateCatDto extends IntersectionType(
CreateCatDto,
AdditionalCatInfo,
) {}
Gợi ý Hàm IntersectionType() được import từ package @nestjs/mapped-types.
Các hàm tiện ích type mapping này có thể kết hợp với nhau. Ví dụ, đoạn code sau sẽ tạo ra một type (class) có tất cả property của type CreateCatDto trừ name, và các property đó sẽ được đặt thành tùy chọn:
export class UpdateCatDto extends PartialType(
OmitType(CreateCatDto, ['name'] as const),
) {}
Ghi chú người dịch Nhóm hàm PartialType/PickType/OmitType/IntersectionType bản chất là “type algebra” chạy lúc runtime (chúng tạo ra class mới và copy metadata decorator sang), khác với TypeScript utility type thuần compile-time như Partial<T>/Pick<T, K> mà bạn quen dùng ở phía frontend React - lý do là class-validator cần các decorator thật sự tồn tại trên class lúc runtime để chạy validate, nên không thể chỉ dùng type-level trick của TypeScript.
Parse và kiểm tra mảng (Parsing and validating arrays)
TypeScript không lưu metadata về generic hay interface, nên khi bạn dùng chúng trong DTO, ValidationPipe có thể không kiểm tra dữ liệu đến một cách đúng đắn. Ví dụ, trong đoạn code sau, createUserDtos sẽ không được kiểm tra đúng cách:
@Post()
createBulk(@Body() createUserDtos: CreateUserDto[]) {
return 'This action adds new users';
}
Để kiểm tra mảng, tạo một class riêng chứa một property bọc lấy mảng đó, hoặc dùng ParseArrayPipe.
@Post()
createBulk(
@Body(new ParseArrayPipe({ items: CreateUserDto }))
createUserDtos: CreateUserDto[],
) {
return 'This action adds new users';
}
Ngoài ra, ParseArrayPipe cũng hữu ích khi parse query parameter. Hãy xem xét phương thức findByIds() trả về user dựa trên các định danh được truyền qua query parameter.
@Get()
findByIds(
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[],
) {
return 'This action returns users by ids';
}
Cấu trúc này kiểm tra các query parameter đến từ một request HTTP GET như sau:
GET /?ids=1,2,3
WebSocket và Microservice
Dù chương này minh họa các ví dụ dùng ứng dụng kiểu HTTP (ví dụ, Express hoặc Fastify), ValidationPipe hoạt động giống hệt với WebSocket và microservice, bất kể phương thức truyền tải nào được sử dụng.
Ghi chú người dịch Điều này thể hiện đúng tinh thần “pipe là một khái niệm độc lập với transport layer” của Nest - khác với middleware kiểu Express chỉ gắn được vào luồng HTTP request/response (req, res, next), pipe của Nest hoạt động trên ExecutionContext trừu tượng, nên cùng một ValidationPipe tái sử dụng được cho WebSocket gateway hay microservice handler mà không cần viết lại logic validate.
Tìm hiểu thêm
Đọc thêm về custom validator, thông báo lỗi, và các decorator có sẵn do package class-validator cung cấp ở đây.