Serialization (tuần tự hóa) là quá trình diễn ra trước khi object được trả về trong response của network. Đây là nơi thích hợp để cung cấp các quy tắc transform và sanitize dữ liệu trả về cho client. Ví dụ, dữ liệu nhạy cảm như password luôn phải bị loại khỏi response. Hoặc, một số property có thể cần transform thêm, chẳng hạn chỉ gửi một tập con các property của một entity. Thực hiện các transformation này thủ công có thể tốn công và dễ sai sót, khiến bạn không chắc đã bao quát hết mọi trường hợp.
Tổng quan
Nest cung cấp sẵn khả năng giúp đảm bảo các thao tác này được thực hiện theo cách đơn giản, rõ ràng. Interceptor ClassSerializerInterceptor sử dụng package class-transformer mạnh mẽ để cung cấp một cách transform object khai báo (declarative) và có thể mở rộng. Thao tác cơ bản mà nó thực hiện là lấy giá trị được trả về bởi một method handler rồi áp dụng function instanceToPlain() từ class-transformer. Khi làm vậy, nó có thể áp dụng các quy tắc được biểu diễn bằng decorator của class-transformer trên một entity/DTO class, như mô tả bên dưới.
Nest cũng bao gồm sẵn StandardSchemaSerializerInterceptor để định hình response theo hướng schema-first. Dùng nó khi bạn muốn response đi ra được validate hoặc transform bởi một schema tương thích Standard Schema thay vì dùng decorator của class-transformer.
Gợi ý Serialization không áp dụng cho response StreamableFile.
Ghi chú người dịch Cơ chế này giống hệt tinh thần của “API Resource” trong Laravel (UserResource quyết định field nào lộ ra ngoài) hoặc DTO response riêng biệt với model ở Go/Express - chỗ khác là ở Nest bạn không cần viết mapper thủ công, chỉ cần gắn decorator lên entity rồi để interceptor lo phần còn lại. Cạm bẫy thường gặp: nếu bạn quên @UseInterceptors(ClassSerializerInterceptor) (hoặc không bind global), decorator @Exclude() sẽ hoàn toàn không có tác dụng và password vẫn lọt ra response như thường.
Loại bỏ property (Exclude properties)
Giả sử ta muốn tự động loại bỏ property password khỏi một user entity. Ta annotate entity như sau:
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
@Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
Giờ xét một controller với một method handler trả về một instance của class này.
@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
return new UserEntity({
id: 1,
firstName: 'John',
lastName: 'Doe',
password: 'password',
});
}
Cảnh báo Lưu ý rằng ta phải trả về một instance của class. Nếu bạn trả về một plain JavaScript object, ví dụ { user: new UserEntity() }, object đó sẽ không được serialize đúng cách.
Gợi ý ClassSerializerInterceptor được import từ @nestjs/common.
Khi endpoint này được gọi, client sẽ nhận được response sau:
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
Lưu ý rằng interceptor này có thể được áp dụng toàn ứng dụng (application-wide) (như đã trình bày tại đây). Sự kết hợp giữa interceptor và khai báo entity class đảm bảo rằng bất kỳ method nào trả về UserEntity đều chắc chắn loại bỏ property password. Điều này cho bạn một mức độ enforce tập trung cho business rule này.
Ghi chú người dịch Đây là điểm khác biệt lớn so với cách làm thủ công kiểu Express (delete user.password trước khi res.json()) - vì rule nằm ở decorator trên entity chứ không nằm rải rác ở từng controller, bạn không thể vô tình quên strip password ở một route mới thêm sau này, miễn là interceptor được bind global.
Expose property
Bạn có thể dùng decorator @Expose() để cung cấp alias name cho property, hoặc để thực thi một function tính giá trị property (tương tự hàm getter), như dưới đây.
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
Transform
Bạn có thể thực hiện thêm transform dữ liệu bằng decorator @Transform(). Ví dụ, cấu trúc sau trả về property name của RoleEntity thay vì trả về toàn bộ object.
@Transform(({ value }) => value.name)
role: RoleEntity;
Truyền option (Pass options)
Bạn có thể muốn thay đổi hành vi mặc định của các transformation function. Để override setting mặc định, truyền chúng trong một object options với decorator @SerializeOptions().
@SerializeOptions({
excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
return new UserEntity();
}
Gợi ý Decorator @SerializeOptions() được import từ @nestjs/common.
Option truyền qua @SerializeOptions() được truyền làm tham số thứ hai của function instanceToPlain() bên dưới. Trong ví dụ này, ta tự động loại bỏ mọi property bắt đầu bằng tiền tố _.
@SerializeOptions() cũng có thể dùng với StandardSchemaSerializerInterceptor:
@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({ schema: userResponseSchema })
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
Bạn cũng có thể truyền validateOptions nếu thư viện schema của bạn hỗ trợ thêm các option validate thông qua interface Standard Schema.
Transform plain object
Bạn có thể ép transformation ở cấp controller bằng cách dùng decorator @SerializeOptions. Điều này đảm bảo tất cả response đều được transform thành instance của class được chỉ định, áp dụng mọi decorator từ class-validator hoặc class-transformer, kể cả khi plain object được trả về. Cách tiếp cận này giúp code sạch hơn, không cần lặp lại việc khởi tạo class hay gọi plainToInstance.
Trong ví dụ dưới đây, dù trả về plain JavaScript object ở cả hai nhánh điều kiện, chúng vẫn sẽ tự động được convert thành instance UserEntity, với các decorator liên quan được áp dụng:
@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({ type: UserEntity })
@Get()
findOne(@Query() { id }: { id: number }): UserEntity {
if (id === 1) {
return {
id: 1,
firstName: 'John',
lastName: 'Doe',
password: 'password',
};
}
return {
id: 2,
firstName: 'Kamil',
lastName: 'Mysliwiec',
password: 'password2',
};
}
Gợi ý Bằng cách chỉ định return type kỳ vọng cho controller, bạn có thể tận dụng khả năng type-checking của TypeScript để đảm bảo plain object trả về đúng hình dạng của DTO hoặc entity. Function plainToInstance không cung cấp mức type hinting này, điều có thể dẫn tới bug tiềm ẩn nếu plain object không khớp với cấu trúc DTO hoặc entity mong đợi.
Ví dụ
Một ví dụ hoạt động đầy đủ có tại đây.
WebSockets và Microservices
Dù chương này minh họa bằng các ví dụ dùng ứng dụng kiểu HTTP (ví dụ Express hoặc Fastify), ClassSerializerInterceptor hoạt động tương tự với WebSockets và Microservices, bất kể transport method nào được sử dụng.
Tìm hiểu thêm
Đọc thêm về các decorator và option có sẵn do package class-transformer cung cấp tại đây.
Nếu bạn thích serialization theo hướng schema-driven hơn, dùng StandardSchemaSerializerInterceptor kết hợp với @SerializeOptions() và option schema của nó.