Setup Boilerplate cho dự án NestJS - Phần 3: Request validation với class-validator và response serialization với class-transformer
Đây là bài viết nằm trong Series NestJS thực chiến, các bạn có thể xem toàn bộ bài viết ở link: https://viblo.asia/s/nestjs-thuc-chien-MkNLr3kaVgA
Đặt vấn đề
Hiện nay việc xử lý dữ liệu user gửi lên API và dữ liệu trả về từ API luôn luôn là một việc không thể thiếu trong quá trình phát triển dự án, hôm nay chúng ta sẽ cùng tìm hiểu 2 package support rất tốt cho NestJS ở lĩnh vực này. Đó chính là class-validator và class-transformer, cả hai thư viện này giúp cho việc xác thực và chuyển đổi dữ liệu trở nên dễ dàng hơn, tiết kiệm thời gian và giảm thiểu các lỗi liên quan đến dữ liệu.
Sử dụng class-validator giúp chúng ta:
- Dễ dàng xác thực dữ liệu đầu vào từ người dùng hoặc các yêu cầu API.
- Tiết kiệm thời gian và giảm thiểu các lỗi liên quan đến dữ liệu không hợp lệ.
- Tự động xác định các lỗi xảy ra và trả về thông báo lỗi cụ thể cho client.
- Tùy chỉnh các quy tắc xác thực dữ liệu phù hợp với nhu cầu của dự án.
Tương tự class-transformer sẽ mang đến các công dụng:
- Dễ dàng chuyển đổi đối tượng lớp (class instance) sang đối tượng đơn giản (plain object) và ngược lại.
- Chuyển đổi định dạng của các thuộc tính (property) trong đối tượng.
- Xóa hoặc giữ lại các thuộc tính trong đối tượng.
- Tùy chỉnh quá trình chuyển đổi dữ liệu phù hợp với nhu cầu của dự án.
Để thấy rõ hơn công dụng của chúng mang lại, chúng ta sẽ lần lượt đi đến nội dung chi tiết ở các phần bên dưới. Mình sẽ cố gắng diễn tả chi tiết và đầy đủ nhất có thể thay vì các nội dung cơ bản để các bạn có thể tận dụng được tối đa các công năng của chúng.
Thông tin package
- "class-validator": "^0.14.0"
- "class-transformer": "^0.5.1"
Các bạn có thể tải về toàn bộ source code của phần này ở đây
1. Class validator
class-validator cung cấp các decorator để thêm validation cho các thuộc tính (properties) của thông tin gửi lên từ request payload. Ví dụ, nếu chúng ta muốn đảm bảo rằng property email phải có định dạng hợp lệ, chúng ta có thể sử dụng decorator @IsEmail()
của class-validator. Thường chúng ta sẽ dùng validate request (params, query, body payload, ...) để đảm bảo dữ liệu user gửi lên là hợp lệ.
Với params và query thì mình thường dùng với Pipe dạng Parse*.
1.1 Cài đặt
Quá trình validation sẽ sử dụng ValidationPipe của NestJS, và nó cần sự kết hợp của class-validator và class-transfomer nên chúng ta phải cài đặt cả 2 package này.
npm i --save class-validator class-transformer
Mình sẽ sử dụng cho toàn bộ ứng dụng nên sẽ apply vào application level hay Global Pipe. Bạn nào chưa đọc về Request Lifecycle có thể xem lại bài viết trước của mình ở đây.
import { Logger, ValidationPipe } from '@nestjs/common';
...
async function bootstrap() {
...
app.useGlobalPipes(new ValidationPipe()) // <== Thêm vào đây
await app.listen(config_service.get('PORT'), () =>
logger.log(`Application running on port ${config_service.get('PORT')}`),
);
}
bootstrap();
Có nhiều option có thể truyền vào tham số của ValidationPipe, tuy nhiên dưới đây là những option mình hay sử dụng trong dự án:
whitelist
: nếu là true, sẽ loại bỏ các property không được liệt kê với class-validator.forbidNonWhitelisted
: nếu là true thì thay vì bỏ qua bởiwhitelist
sẽ trả về lỗiskipMissingProperties
: nếu property truyền vào không tồn tại hoặc có giá trịundefined/null
sẽ bỏ qua validate cho property đó.groups
: một số schema sẽ có hơn 1 loại validation, groups giúp chúng ta phân loại, thực thi cho từng group khác nhau. Option này thường được dùng cho các Pipe bên trong Global Pipe.
1.2 Validate object
Chúng ta sẽ bắt đầu bằng việc validate một object đơn giản, tiến hành thêm các decorator cho CreateUserDto
để validate các property:
import {
IsEmail,
IsNotEmpty,
IsStrongPassword,
MaxLength,
} from 'class-validator';
export class CreateUserDto {
@IsNotEmpty() // Bắt buộc phải gửi lên
@MaxLength(50) // Tối đa 50 ký tự
first_name: string;
@IsNotEmpty()
@MaxLength(50)
last_name: string;
@IsNotEmpty()
@MaxLength(50)
@IsEmail() // Phải là định dạng email
email: string;
@IsNotEmpty()
@MaxLength(50)
username: string;
@IsNotEmpty()
@IsStrongPassword() // Password phải đủ độ mạnh
password: string;
@IsOptional() // Không bắt buộc phải gửi lên
address?: CreateAddressDto[];
}
Lưu ý: Trước khi tiếp tục mình muốn lưu ý các bạn một trường hợp mà khi làm việc với NestJS rất có thể các bạn sẽ mắc phải. Đó là khi generate code từ NestCLI sẽ có dạng
this.users_repository.create(create_user_dto)
để lưu data từCreateUserDto
vào database, việc này dẫn đến một cơ hội để ai đó có thể lợi dụng. Ví dụ trong User schema có propertypoint
là số tiền user nạp vào, nếu chúng ta dùng luôn như trên mà không chỉnh sửa gì hoặc chỉnh sửa và dùng spread operatorthis.users_repository.create({password: hash_password, ...create_user_dto})
thì khi ai đó gửi kèmpoint: 99999999
sẽ dẫn đến số point của user sẽ thành con số mà họ gửi lên (mình lấy ví dụ để minh họa thôi chứ thực tế chắc ít ai để logic code tạo user như vậy 😅). Có nhiều cách để giải quyết vấn đề trên như:
- Truyền từng property hợp lệ vào function
create
thay vì toàn bộ property trongCreateUserDto
: cách này ổn nhưng nếu schema nào có nhiều property sẽ làm code dài dòng.- Dùng delete với object để delete các property không hợp lệ sau đó mới truyền vào function
create
: tương ứng với n các property cần xóa sẽ có n lệnh delete được tạo ra.- Tạo Factory Pattern để tạo các property hợp lệ: cách này hay và làm cho code rõ ràng dễ đọc hơn, tuy nhiên phải mất thời gian viết thêm Factory cho từng schema.
- Dùng desctructuring (
const {point, role, ...process_create_user_dto} = create_user_dto
): cách này là cải tiến của delete ở trên, khá ổn và có thể sử dụng. Nhưng đôi khi nếu không cẩn thận để sót property cần xóa thì cũng như không.- Sử dụng option
whitelist
củaValidationPipe
: đây là cách hiện tại mình đang dùng, chỉ cần thêm option đó vào, các property nào không có decorator của class-validator sẽ bị bỏ qua. Đảm bảo chỉ những property trongCreateUserDto
mới có thể đi quaValidationPipe
. Nếu không có decorator nào phù hợp cho property chúng ta có thể dùng decorator@Allow
.
- Gọi POST http://localhost:3333/users để tạo user và kiểm tra kết quả
- Các property không hợp lệ sẽ trả về lỗi
- Thử với trường hợp dữ liệu hợp lệ và kèm với
point
. Có thể thấy dù đã gửi lên nhưng sẽ bịValidationPipe
loại ra
Tiếp theo chúng ta sẽ đến với UpdateUserDto
import { OmitType, PartialType } from '@nestjs/swagger';
import {
IsDateString,
IsEnum,
IsOptional,
IsPhoneNumber,
MaxLength,
} from 'class-validator';
import { GENDER } from '../entities/user.entity';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['email', 'password', 'username']),
) {
@IsOptional()
@IsPhoneNumber()
phone_number?: string;
@IsOptional()
@IsDateString()
date_of_birth?: Date;
@IsOptional()
@IsEnum(GENDER)
gender?: string;
@IsOptional()
@MaxLength(200)
headline?: string;
}
- Vì là update đa số chỉ cần cập nhật một số property nên chúng ta sẽ dùng
PartialType
chuyển các property củaCreateUserDto
về trạng thái optional. Tuy nhiên đôi khi chúng ta cũng sẽ cần loại bỏ một số property tùy theo logic nghiệp vụ như username, email, password để tránh không cho user cập nhật trong các API nhất định, do đó mình dùngOmitType
để loại bỏ các property đó.
Các method hữu ích khác như:
PickType(DTOObject, ['field_name'] as const)
,IntersectionType(DTO1, DTO2)
- Lưu ý: mình dùng mapped types từ
@nestjs/swagger
chứ không dùng của@nestjs/mapped-types
vì trong series này các bài tiếp theo mình sẽ dùng Swagger cho API docs và theo khuyến nghị từ tài liệu của NestJS "Therefore, if you used@nestjs/mapped-types
(instead of an appropriate one, either@nestjs/swagger
or@nestjs/graphql
depending on the type of your app), you may face various, undocumented side-effects.". Nên để có thể sử dụng các bạn cài đặt package này giúp mình:- Cài đặt @nestjs/swagger:
npm install --save @nestjs/swagger
- Chúng ta cũng cần thêm vào
"plugins": ["@nestjs/swagger"]
trong nest-cli.json do khác với @nestjs/mapped-types, mặc địnhPartialType
của @nestjs/swagger không làm cho các property optional ( xem thêm ở đây ).{ "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, "plugins": ["@nestjs/swagger"] // <=== Thêm vào đây } }
- Cài đặt @nestjs/swagger:
- Gọi đến API PATCH http://localhost:3333/users/:id để cập nhật user và kiểm tra kết quả
- Với dữ liệu không hợp lệ
- Dữ liệu hợp lệ thì các trường được thêm vào sẽ được cập nhật. Có thể thấy chúng ta cập nhật được cả field
first_name
từCreateUserDto
.
1.3 Validate nested object
Chúng ta sẽ đến với một trường hợp cũng rất hay gặp phải, payload gửi đi có bao gồm nested object, khi đó chúng ta cũng cần đảm bảo giá trị gửi lên bên trong nested object phải được validate. Ví dụ cụ thể với property address
bên trong user mà chúng ta đã dùng để minh họa quan hệ one-to-one ở bài viết trước.
- Đầu tiên chúng ta cập nhật lại
CreateAddressDto
để validate cho các property truyền vào khi cần tạo address. Cách validation cũng tương tự như các module trên, chỉ cópostal_code
chúng ta sẽ dùng methodIsPostalCode
để đảm bảo tính thực tế.
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsPostalCode,
MaxLength,
MinLength,
} from 'class-validator';
export class CreateAddressDto {
@IsOptional()
@MinLength(2)
@MaxLength(120)
street?: string;
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
state: string;
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
city: string;
@IsOptional()
@IsNumber()
@IsPostalCode('US')
postal_code: number;
@IsNotEmpty()
@MinLength(2)
@MaxLength(50)
country: string;
}
- Sau đó thêm
CreateAddressDto
vàoCreateUserDto
với property address
import { CreateAddressDto } from './create-address.dto';
import { Type } from 'class-transformer';
...
export class CreateUserDto {
...
@IsOptional()
@ValidateNested()
@Type(() => CreateAddressDto)
address?: CreateAddressDto;
}
- Giải thích:
ValidateNested
: đầu tiên để validate nested object chúng ta cần dùng decorator này, giúp class-validator đi đến các object bên trong.Type
:ValidateNested
thôi thì vẫn chưa đủ, dữ liệu address được gửi lên ban đầu ở dạng plain object vì thế cần được transform về instance củaCreateAddressDto
để class-validator có thể validate.Type
được import từ package class-transformer mà phần sau chúng ta sẽ tìm hiểu.
Do đó chỉ cần thiếu một trong 2 decorator trên thì validate cho property đó sẽ bị bỏ qua.
- Tiến hành test thử:
- Thiếu các property bắt buộc sẽ báo lỗi:
- Không truyền address trong lúc tạo thì sẽ bỏ qua validate cho address do chúng ta dùng
IsOptional
: - Trường hợp tắt 1 trong 2 decorator
ValidateNested
hoặcType
thì dù gửi address lên nhưng validate cho property của address vẫn sẽ bị bỏ qua, vì thế chúng ta gặp validate ở mongoose.
1.4 Validate Array
Trong trường hợp các bạn muốn validate các item bên trong Array, chẳng hạn các item phải thuộc enum nào đó . Ví dụ chúng ta sẽ thêm property interested_languages
cho User schema để biểu thị các ngon ngữ mà user hứng thú. Khi tạo user sẽ gửi các ngôn ngữ đó với payload.
- Thêm property
interested_languages
vào user.entity.ts
export enum LANGUAGES {
ENGLISH = 'English',
FRENCH = 'French',
JAPANESE = 'Japanese',
KOREAN = 'Korean',
SPANISH = 'Spanish',
}
...
@Prop({
type: [String],
enum: LANGUAGES,
})
interested_languages: LANGUAGES[];
...
- Cập nhật
CreateUserDto
, đảm bảo nếu user gửi lêninterested_languages
thì mảng phải có ít nhất 1 phần tử và các phần tử mảng phải thuộc enumTOPIC
import { LANGUAGES } from '../entities/user.entity';
...
export class CreateUserDto {
@IsOptional() // Không bắt buộc
@IsArray() // Nếu có dữ liệu phải là dạng array
@ArrayMinSize(1) // Array phải có tối thiểu 1 phần tử
@IsEnum(LANGUAGES, { each: true }) // Các phần tử của array phải thuộc enum LANGUAGES
interested_languages: LANGUAGES[];
...
- Giải thích:
IsOptional
: vì chúng ta không bắt buộc user phải gửi address nên nếu không có thì sẽ bỏ qua không cần các trigger các decorator bên dưới.IsArray
: bắt buộc data gửi lên phải là arrayArrayMinSize
: để tránh trường hợp user gửi mảng rỗng ( trong ví dụ của chúng ta không cần check cái này cũng được, nhưng trong một vài trường hợp thực tế phải đảm bảo mảng gửi lên không được rỗng )IsEnum( each: true )
: option each giúp validate từng phần tử trong mảng
Validate Array Object
Chúng ta đến với trường hợp phức tạp hơn khi item bên trong Array là Object và bắt buộc cần phải validate. Ví dụ có yêu cầu từ phía khách hàng cần thay đổi cho phép user lưu nhiều address, khi này chúng ta sẽ chỉnh sửa lại:
- Chỉnh sửa lại property address trong User Schema thành Array
...
@Prop({
type: [
{
type: AddressSchema,
},
],
})
address: Address[];
...
- Cập nhật
CreateUserDto
, đảm bảo nếu user gửi lên address thì mảng phải có ít nhất 1 phần tử và các phần tử mảng phải thuộc validate vớiCreateAddressDto
...
export class CreateUserDto {
...
@IsOptional()
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => CreateAddressDto)
address?: CreateAddressDto[];
...
- Giải thích: chúng ta sẽ dùng kết hợp giữa validate nested object và validate array để giải quyết trường hợp này. Tất nhiên các bạn không được bỏ sót
ValidateNested
hoặcType
như đã nói ở trên.
2. Serialization với class-transformer
Trong NestJS, class-transformer thường được sử dụng để thực hiện việc chuyển đổi đối tượng lớp (class instances) sang đối tượng đơn giản (plain objects) và ngược lại, giúp thao tác với dữ liệu trở nên dễ dàng cũng như điều chỉnh lại reponse phù hợp trước khi trả về cho người dùng. Cụ thể các
2.1 Cài đặt
Chúng ta đã cài đặt class-transformer ở trên cùng với class-validator.
2.2 Cách sử dụng
Ví dụ ở đây mình sẽ thêm property stripe_customer_id
để sau này dùng cho bài viết thanh toán với Stripe, sau đó sử dụng decorator @Exclude
loại bỏ property đó khỏi các response.
import { Exclude } from 'class-transformer';
...
@Prop({
default: 'cus_mock_id',
})
@Exclude()
stripe_customer_id: string;
Tạo lại user xem property stripe_customer_id
có bị loại bỏ chưa.
- Có thể thấy vẫn chưa như chúng ta mong đợi, chúng ta cần thêm một bước là dùng Interceptors nữa mới có thể apply logic trên. Ở đây mình sẽ apply cho toàn bộ các API trong user module nên dùng
ClassSerializerInterceptor
cho Controller Interceptor.
import { UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
...
- Thử gọi API get lại user chúng ta vừa tạo
- Lần này không những property
stripe_customer_id
không mất mà còn kéo theo sự xuất hiện của hàng loạt property khác của MongoDB. Nguyên nhân không phải do chúng ta sai, mà là do response của MongoDB không tương thích với cách mà class-transformer response (nếu các bạn dùng các hệ quản trị cơ sở dữ liệu khác thì sẽ không bị vấn đề này). - Để giải quyết vấn đề, chúng ta cần custom lại
ClassSerializerInterceptor
để đồng nhất cách response giữa 2 package trên.
import {
ClassSerializerInterceptor,
PlainLiteralObject,
Type,
} from '@nestjs/common';
import { ClassTransformOptions, plainToClass } from 'class-transformer';
import { Document } from 'mongoose';
function MongooseClassSerializerInterceptor(
classToIntercept: Type,
): typeof ClassSerializerInterceptor {
return class Interceptor extends ClassSerializerInterceptor {
private changePlainObjectToClass(document: PlainLiteralObject) {
if (!(document instanceof Document)) {
return document;
}
return plainToClass(classToIntercept, document.toJSON());
}
private prepareResponse(
response:
| PlainLiteralObject
| PlainLiteralObject[]
| { items: PlainLiteralObject[]; count: number },
) {
if (!Array.isArray(response) && response?.items) {
const items = this.prepareResponse(response.items);
return {
count: response.count,
items,
};
}
if (Array.isArray(response)) {
return response.map(this.changePlainObjectToClass);
}
return this.changePlainObjectToClass(response);
}
serialize(
response: PlainLiteralObject | PlainLiteralObject[],
options: ClassTransformOptions,
) {
return super.serialize(this.prepareResponse(response), options);
}
};
}
export default MongooseClassSerializerInterceptor;
- Giải thích:
- Chúng ta sẽ tạo ra 1 HoC với mục đích nhận vào schema.
- Sau đó chúng ta return về custom class kế thừa
ClassSerializerInterceptor
để overide lại methodserialize
của nó. - Các bạn chú ý để method
prepareResponse
, đây là nơi chúng ta kiểm tra trước khi serialize response.- Ở dòng
if (!Array.isArray(response) && response?.items)
mình dùng để xử lý cho methodfindAll
. Chúng ta gọi đệ quy bên trong dùng cho trường hợp schema có nested object bên trong (ví dụ user bên trong flash-card). - Dòng
if (Array.isArray(response))
dùng để trả về cho trường hợp response là array (ví dụ address bên trong user hoặc vớifindAll
sau khi đệ quy ở if đầu tiên sẽ gặp dòng if này để xử lí tiếp).
- Ở dòng
- Với method
changePlainObjectToClass
sẽ là nơi chính chúng ta giải quyết vấn đề không đồng nhất giữa mongoose và class-transfomer.if (!(document instanceof Document))
nếu response không phải là Document của mongoose thì chúng ta sẽ trả về. Ngược lại nếu là Document chúng ta cần chuyển nó về JSON sau đó transfer sang Class của Schema đó.
import { UseInterceptors } from '@nestjs/common';
import { User } from './entities/user.entity';
import { MongooseClassSerializerInterceptor } from 'src/interceptors/mongoose-class-serializer.interceptor';
@UseInterceptors(MongooseClassSerializerInterceptor(User)) // Lưu ý không được quên User schema
export class UsersController {
...
- Thử lại xem kết quả đã hoạt động chưa
Kết quả đã thành công loại bỏ các property mình muốn khỏi response. Có nhiều option khác để các bạn có thể sử dụng như:
- Đối với các schema cần loại bỏ nhiều property thì chúng ta có thể dùng @Exclude sau đó dùng
@Expose
để hiển thị các property cần thiết
import { Exclude, Expose } from 'class-transformer';
...
@Exclude()
export class UserRole extends BaseEntity {
@Prop({
unique: true,
default: USER_ROLE.USER,
enum: USER_ROLE,
required: true,
})
@Expose()
name: USER_ROLE;
@Prop()
description: string;
}
...
- Trong trường hợp các bạn thiết kế các property private bắt đầu với prefix "" thì có thể dùng option như sau
excludePrefixes: ["_"]
để mặc định loại bỏ các property bắt đầu bằng "".
...
export function MongooseClassSerializerInterceptor(
classToIntercept: Type,
): typeof ClassSerializerInterceptor {
return class Interceptor extends ClassSerializerInterceptor {
private changePlainObjectToClass(document: PlainLiteralObject) {
if (!(document instanceof Document)) { return document }
return plainToClass(classToIntercept, document.toJSON(), { excludePrefixes: ['_'] });
}
...
2.3 Các case thông dụng
planToClass
Phương thức này chuyển đổi một javascript object thành instance của class cụ thể. Ở changePlainObjectToClass
chúng ta đã chuyển Document về JSON sau đó mới dùng planToClass để chuyển lại instance của input class.
Transform
Được dùng cho trường hợp khi gọi API users có bao gồm cả object của role, như vậy thì đôi khi dài dòng và không hay, nên chúng ta có thể chỉnh lại cho ngắn gọn hơn như bên dưới.
import { Transform, Type } from 'class-transformer';
...
export class User extends BaseEntity {
...
@Prop({
type: mongoose.Schema.Types.ObjectId,
ref: UserRole.name,
})
@Type(() => UserRole)
@Transform((value) => value.obj.role?.name, { toClassOnly: true })
role: UserRole;
Kết quả sau khi transform, property role
chuyển từ role: { name: "User" }
sang role: "User"
như bên dưới
Working with nested objects
Đây cũng là trường hợp được sử dụng thông dụng mà các bạn thường hay bắt gặp. Ví dụ, giả sử với property address của user, chúng ta muốn ẩn đi property postal_code
đối với request gọi đến các API của user module. Nếu bình thường chúng ta chỉ dùng @Exclude
thôi thì vẫn chưa đủ.
import { Exclude } from 'class-transformer';
...
export class Address extends BaseEntity {
...
@Prop({ required: false, minlength: 2, maxlength: 50 })
@Exclude()
postal_code?: number;
...
Khi tạo, cập nhật hoặc get user thì property postal_code
vẫn được trả về trong address
.
Nguyên nhân là do "Since Typescript does not have good reflection abilities yet, we should implicitly specify what type of object each property contain". Để giải quyết vấn đề này chúng ta cần thêm vào decorator @Type
cho property address trong user schema.
import { Exclude, Expose, Type } from 'class-transformer';
...
export class User extends BaseEntity {
@Prop({
type: [
{ type: AddressSchema },
],
})
@Type(() => Address)
address: Address[];
...
Thử lại với những gì vừa chỉnh sửa, có thể thấy property postal_code
đã được ẩn đi.
Exposing properties with different names
Ví dụ chúng ta expose ra getter fullName
để truy xuất tên đầy đủ của user, sau đó dùng option name
để đổi lại tên theo ý chúng ta muốn
...
export class User extends BaseEntity {
...
@Expose({ name: 'full_name' })
get fullName(): string {
return `${this.first_name} ${this.last_name}`;
}
...
Kết quả
Lưu ý: nếu dùng với các property thì phải sử dụng với option toPlainOnly: true
, nếu không property sẽ biến mất thay vì được đổi tên.
- Ví dụ mình sẽ rename property email sang mail như bên dưới, property email sẽ bị mất trong response.
import { Exclude, Expose, Type } from 'class-transformer';
...
export class User extends BaseEntity {
@Prop({
required: true,
unique: true,
match: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
})
@Expose({ name: 'mail'})
email: string;
...
- Thêm vào option
toPlainOnly: true
để giải quyết vấn đề trên
import { Exclude, Expose, Type } from 'class-transformer';
...
export class User extends BaseEntity {
@Prop({
required: true,
unique: true,
match: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
})
@Expose({ name: 'mail', toPlainOnly: true })
email: string;
...
Tuy nhiên trong trường hợp nếu dùng
@Exclude
để loại bỏ toàn bộ property sau đó@Expose
các property cần hiển thị thì không được dùng option name, sẽ gặp lỗi mất property như trên. Mình đang tìm nguyên nhân, các bạn nào hiểu rõ có thể comment giải thích giúp mình và mọi người nha. Ví dụ như bên dưới sẽ làm mất property name mặc dù chúng ta sử dụngtoPlainOnly
... @Exclude() export class UserRole extends BaseEntity { @Prop({...}) @Expose({ name: 'role', toPlainOnly: true }) name: string; ...
Skipping specific properties
Có 2 cách như chúng ta đã ví dụ ở trên
- Dùng
@Exclude
cho 1 property cụ thể để loại bỏ property đó. - Dùng
@Exclude
cho toàn schema để loại bỏ tất cả các property, sau đó@Expose
các property còn lại để hiển thị chúng ra.
Skipping some prefixed properties
Chúng ta đã sử dụng excludePrefixes
trong MongooseClassSerializerInterceptor
để loại bỏ các property bắt đầu bằng "_", các bạn có thể thêm vào các prefix khác trong array value của excludePrefixes
để loại bỏ hàng loạt các property mình muốn.
Hoặc các bạn có thể dùng decorator @SerializeOptions
với option excludePrefixes
cho các API cụ thể để loại bỏ các property mình muốn dựa vào prefix. Ví dụ mình sẽ loại bỏ property bắt đầu bằng first
và last
trong API findAllUsers.
import { SerializeOptions } from '@nestjs/common';
...
export class UsersController {
...
@SerializeOptions({
excludePrefixes: ['first', 'last'],
})
@Get()
findAll() { return this.users_service.findAll(); }
...
-
Gọi http://localhost:3333/users, các property đã mất theo dự định của chúng ta.
-
Tuy nhiên ở đây có một vấn đề phát sinh, property
_id
đã biến mất. Chúng ta cần expose ra id để dùng cho các mục đích khác, chỉnh sửa lạiBaseEntity
để giải quyết vấn đề trên.import { Prop } from '@nestjs/mongoose'; import { Expose, Transform } from 'class-transformer'; export class BaseEntity { @Expose() @Transform((value) => value.obj?._id?.toString(), { toClassOnly: true }) id?: string; ... }
-
Thử lại xem response đã có
id
như chúng ta mong muốn hay chưa
Kết luận
Trong bài viết này chúng ta đã thực hiện việc cài đặt và cấu hình thư viện class-validator và class-transformer trong ứng dụng NestJS. Tiếp theo, chúng ta đã tạo một DTO (Data Transfer Object) để validate các trường của request và kết hợp với class-validator để thực hiện validation. Sau đó, chúng ta đã sử dụng class-transformer để serialize đối tượng response trả về từ controller, giúp chúng ta có thể tuỳ chỉnh định dạng của response dựa trên các quy tắc chuyển đổi được định nghĩa sẵn. Cuối cùng, chúng ta đã tìm hiểu về cách sử dụng ValidationPipe để tự động xác thực các request và serialize các response trả về. Với ValidationPipe, chúng ta có thể tuỳ chỉnh các tùy chọn và quy tắc validation theo ý muốn.
Tuy nhiên, việc bảo mật ứng dụng không chỉ dừng lại ở việc validate request và serialize response. Trong bài viết tiếp theo, chúng ta sẽ tìm hiểu về cách thực hiện xác thực với JWT sử dụng thuật toán bất đối xứng dùng package crypto của NodeJS mới. Điều này giúp chúng ta đảm bảo tính an toàn cho các request được gửi đến từ client và tăng cường tính bảo mật cho ứng dụng của mình.
Bài viết mới này sẽ giúp bạn hiểu rõ hơn về JWT, cách sử dụng thuật toán bất đối xứng để mã hóa và giải mã token, cách áp dụng JWT trong NestJS để xác thực và bảo mật các request gửi đến từ client. Hãy tiếp tục theo dõi series của chúng ta để tìm hiểu thêm về chủ đề này.
Tài liệu tham khảo
- Documentation: Nestjs - a progressive node.js framework (no date) NestJS. Available at: https://docs.nestjs.com/techniques/validation (Accessed: 12 May 2023).
- Typestack (no date) Typestack/class-validator: Decorator-based property validation for classes., GitHub. Available at: https://github.com/typestack/class-validator (Accessed: 12 May 2023).
- Typestack (no date a) Typestack/class-transformer: Decorator-based transformation, serialization, and deserialization between objects and classes., GitHub. Available at: https://github.com/typestack/class-transformer (Accessed: 12 May 2023).
All rights reserved