掌握 NestJS 中的資料驗證:類別驗證器和類別轉換器的完整指南
介紹
在快節奏的開發世界中,資料完整性和可靠性至關重要。強大的資料驗證和有效的使用者資料處理可以帶來流暢的體驗和不一致的應用程式狀態之間的差異。
以下引用 George Fuechsel 的話總結了本文的內容。
“垃圾進來,垃圾出去。” — 喬治·富克塞爾
在本文中,我們將深入研究 NestJS 中的資料驗證。我們將探索類別驗證器和類別轉換器的一些複雜用例,以確保資料有效且格式正確。在此過程中,我們將討論最佳實踐、一些先進技術和常見陷阱,以將您的技能提升到新的水平。我的動機是讓您能夠使用 NestJS 建立更具彈性和防錯的應用程式。
當我們一起經歷這個旅程時,請記住,我們永遠不應該信任應用程式外部的使用者或客戶提交的任何輸入,無論它是否是更大服務(微服務)的一部分。
目錄
- 簡介
- 資料傳輸物件(DTO)。這是什麼?
- 初始設定:設定您的 NestJS 專案
- 建立使用者 DTO
- 將類別驗證器新增至欄位
- 驗證巢狀物件
- 使用 Class-transformer 中的 Transform() 和 Type()
- 條件驗證
- 處理驗證錯誤
- 了解管道
- 設定全域驗證管道
- 格式驗證錯誤
- 建立自訂驗證器
- 自訂密碼驗證器
- 具有自訂驗證選項的非同步自訂驗證器
- 常見陷阱與最佳實務
- 結論
- 其他資源
資料傳輸物件(DTO)。它是什麼?
DTO 是一種我們可以用來封裝資料並將其傳輸到應用程式的不同層的模式。它們對於管理應用程式流入(請求)和流出(回應)的資料非常有用。
不可變的 DTO
正如我們已經確定的,使用 DTO 的主要思想是傳輸數據,因此數據在創建後不應更改。一般來說,DTO 被設計為不可變的,這意味著一旦創建它們,它們的屬性就無法修改。隨之而來的一些好處包括但不限於:
- 可預測的行為:資料保持不變的信心。
- 一致性:一旦創建,其狀態在整個生命週期中保持不變,直到被垃圾收集。
JavaScript 沒有用於建立不可變類型的內建類型,就像 Java 和 C# 中的 record 類型一樣。我們可以透過將欄位設為唯讀來實現類似的行為。
初始配置:設定您的 NestJS 項目
我們將從一個小型使用者管理專案開始,其中包括用於管理使用者的基本 CRUD 操作。如果您想探索完整的原始程式碼,可以點擊此處造訪 GitHub 上的專案。
安裝 NestJS CLI
$ npm i -g @nestjs/cli $ nest new user-mgt
安裝類別驗證器和類別轉換器
npm i --save class-validator class-transformer
產生使用者模組
$ nest g resource users ? What transport layer do you use? REST API ? Would you like to generate CRUD entry points? No
建立一個空的 DTO 和實體資料夾。完成所有操作後,您應該擁有這樣的結構。
建立使用者 DTO
我們先建立必要的 DTO。本教學將只關注兩個操作:建立和更新使用者。在DTO資料夾中建立兩個檔案
user-create.dto.ts
export class UserCreateDto { public readonly name: string; public readonly email: string; public readonly password: string; public readonly age: number; public readonly dateOfBirth: Date; public readonly photos: string[]; }
user-update.dto.ts
import { PartialType } from '@nestjs/mapped-types'; import { UserCreateDto } from './user-create.dto'; export class UserUpdateDto extends PartialType(UserCreateDto) {}
UserUpdateDto 擴展了 UserCreateDto 以繼承所有屬性,PartialType 確保所有欄位都是可選的,允許部分更新。這節省了我們的時間,因此我們不必重複。
將類別驗證器新增至字段
讓我們詳細介紹如何在欄位中新增驗證。類別驗證器為我們提供了許多已經製作好的驗證裝飾器,我們可以將這些規則應用到我們的 DTO 中。現在,我們將使用一些來驗證 UserCreateDto。按一下此處查看完整列表。
import { IsString, IsEmail, IsInt, Min, Max, Length, IsDate, IsArray, ArrayNotEmpty, ValidateNested, IsUrl, } from 'class-validator'; import { Transform, Type } from 'class-transformer'; export class UserCreateDto { @IsString() @Length(2, 30, { message: 'Name must be between 2 and 30 characters' }) @Transform(({ value }) => value.trim()) public readonly name: string; @IsEmail({}, { message: 'Invalid email address' }) public readonly email: string; @IsString() @Length(8, 50, { message: 'Password must be between 8 and 50 characters' }) public readonly password: string; @IsInt() @Min(18, { message: 'Age must be at least 18' }) @Max(100, { message: 'Age must not exceed 100' }) public readonly age: number; @IsDate({ message: 'Invalid date format' }) @Type(() => Date) public readonly dateOfBirth: Date; @IsArray() @ValidateNested() @ArrayNotEmpty({ message: 'Photos array should not be empty' }) @IsString({ each: true, message: 'Each photo URL must be a string' }) @IsUrl({}, { each: true, message: 'Each photo must be a valid URL' }) public readonly photos: string[]; }
我們的簡單類別的大小已經增大,我們使用 Class-Validator 中的裝飾器對欄位進行了註解。這些裝飾器將驗證規則套用至欄位。如果您是新手,您可能會對裝飾器有疑問。例如,它們是什麼意思?讓我們分解一下我們使用過的一些基本驗證器。
- IsString() → This decorator ensures that a value is a string.
- Length(min, max) → This ensures that the string has a link within the specified range.
- IsInt() → This decorator checks if the value is an integer.
- Min() and Max() → This ensures that a numeric value falls between the range
- IsDate() → This ensures that the value is a valid date
- IsArray() → Validates that the value is an array
- IsUrl() → Validate the value is a valid URL
- Transform() → Change the data into a different format
Decorator Parameters
The UserCreateDto fields validator contains additional properties passed into it. These allow you to:
- Customize validation rules
- Provide values
- Set validation options
- Provide messages when the validation fails etc.
Validating Nested Objects
Unlike normal fields validating nested objects requires a bit of extra processing, class-transformer together with class-validator allows you to validate nested objects.
We did a little bit of nested validation in UserCreateDto when we validated the photos field.
@IsArray() @IsUrl({}, { each: true, message: 'Each photo must be a valid URL' }) public readonly photos: string[];
Photos are an array of strings. To validate the nested strings, we added ValidateNested() and { each: true } to ensure that, each link is a valid URL.
Let’s update photos a some-what complex structure. create a new file in DTO folder and name it user-photo.dto.ts
import { IsString, IsInt, Min, Max, IsUrl, Length } from 'class-validator'; export class UserPhotoDto { @IsString() @Length(2, 100, { message: 'Name must be between 2 and 100 characters' }) public readonly name: string; @IsInt() @Min(1, { message: 'Size must be at least 1 byte' }) @Max(5_000_000, { message: 'Size must not exceed 5MB' }) public readonly size: number; @IsUrl( { protocols: ['http', 'https'], require_protocol: true }, { message: 'Invalid URL format' }, ) public readonly url: string; }
Now let’s update the photos section of UserCreateDto
export class UserCreateDto { // Other fields @IsArray() @ArrayNotEmpty({ message: 'Photos array should not be empty' }) @ValidateNested({ each: true }) @Type(() => UserPhotoDto) public readonly photos: UserPhotoDto[]; }
The ValidateNested() decorator ensures that each element in the array is a valid photo object. The most important thing to be aware of when it comes to nested validation is that the nested object must be an instance of a class else ValidateNested() won’t know the target class for validation. This is where class-transformer comes in.
Using Transform() and Type() from Class-transformer
Class-transformer provides us with the @Type() decorator. Since Typescript doesn’t have good reflection capabilities yet, we use @Type(() => UserPhotoDto) to give an instance of the class.
We can also utilize the Type() decorator for basic data transformation in our DTO. The dateOfBirth field in UserCreateDto is transformed into a date object using @Type(() => Date).
For complex DTO fields transformation, the Tranform() decorator handles this perfectly. It allows you to access both the field value and the entire object being validated. Whether you’re converting data types, formatting strings, or applying custom logic, @Transform() gives you the control to return the exact version of the value that your application needs.
@Transform(({ value, obj }) => { // perform additional transformation return value; })
Conditional Validation
Most often, some fields need to be validated based on some business rules, we can use the ValidateIf() decorator, which allows you to apply validation to a field only if some condition is true. This is very useful if a field depends on other fields like multi-step forms.
Let’s update the UserPhotoDto to include an optional description field, which should only be validated if it is provided. If the description is present, it should be a string with a length between 10 and 200 characters.
export class UserPhotoDto { // Other fields @ValidateIf((o) => o.description !== undefined) @IsString({ message: 'Description must be a string' }) @Length(10, 200, { message: 'Description must be between 10 and 200 characters', }) public readonly description?: string; }
Handling Validation Errors
Before we dive into how NestJS handles validation errors, let’s first create simple handlers in the user.controller.ts. We need a basic route to handle user creation.
import { Body, Controller, Post } from '@nestjs/common'; import { UserCreateDto } from './dto/user-create.dto'; @Controller('users') export class UsersController { @Post() createUser(@Body() userCreateDto: UserCreateDto) { // delegating the creation to a service return { message: 'User created successfully!', user: userCreateDto, }; } }
Trying this endpoint on Postman with no payload gives us a successful response.
NestJS has a good integration with class-validator for data validation. Still, why wasn’t our request validated? To tell NestJS that we want to validate UserCreateDto we have to supply a pipe to the Body() decorator.
Understanding Pipes
Pipes are flexible and powerful ways to transform and validate incoming data. Pipes are any class decorated with Injectable() and implement the PipeTransform interface. The usage of pipe we are interested is its ability to check that an incoming request meets a certain criteria or throw errors if otherwise.
The most common way to validate the UserCreateDto is to use the built-in ValidationPipe. This pipe validates rules in your DTO defined with class-validator
Now we pass a validation pipe to the Body() to validate the DTO
import { Body, Controller, Post, ValidationPipe } from '@nestjs/common'; import { UserCreateDto } from './dto/user-create.dto'; @Controller('users') export class UsersController { @Post() createUser(@Body(new ValidationPipe()) userCreateDto: UserCreateDto) { // delegating the creation to services return { message: 'User created successfully!', user: userCreateDto, }; } }
With this small change, we get the errors below if we try to create a user with no payload.
Awesome right :)
Setting Up a Global Validation Pipe
To ensure that all requests are validated across the entire application. We have to set up a global validation pipe so that we don’t have to pass validation pipe to every Body() decorator.
Update main.ts
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, }), ); await app.listen(3000); } bootstrap();
The built-in validation pipe uses class-transformer and class-validator, we can pass validations options to be used by these underlying packages. whitelist: true automatically strips any properties that are not defined in the DTO.transform: true automatically transforms the payload into the appropriate types defined in your DTO.
ValidationPipe({ whitelist: true, transform: true, }),
With this, we can remove the pipe we passed to createUser endpoint and it will still be validated. Passing it to parameters helps us fine-tune the validation we need for specific endpoints.
@Post() createUser(@Body() userCreateDto: UserCreateDto) { // ... }
Formatting Validation Errors
The default validation errors format is not bad, we get to see all the errors for the validations that failed, Some frontend developers will scream at you though for mixing all the errors, I have been there?. Another reason to separate it is when you want to display errors under the fields that failed on the UI.
For nested objects, we also need to retrieve all the errors recursively for a smooth experience. We can achieve this by passing a custom exceptionFactory method to format the errors.
Update main.ts
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { BadRequestException, ValidationError, ValidationPipe, } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, exceptionFactory: (validationErrors: ValidationError[] = []) => { const getPrettyClassValidatorErrors = ( validationErrors: ValidationError[], parentProperty = '', ): Array<{ property: string; errors: string[] }> => { const errors = []; const getValidationErrorsRecursively = ( validationErrors: ValidationError[], parentProperty = '', ) => { for (const error of validationErrors) { const propertyPath = parentProperty ? `${parentProperty}.${error.property}` : error.property; if (error.constraints) { errors.push({ property: propertyPath, errors: Object.values(error.constraints), }); } if (error.children?.length) { getValidationErrorsRecursively(error.children, propertyPath); } } }; getValidationErrorsRecursively(validationErrors, parentProperty); return errors; }; const errors = getPrettyClassValidatorErrors(validationErrors); return new BadRequestException({ message: 'validation error', errors: errors, }); }, }), ); await app.listen(3000); } bootstrap();
This looks way better. Hopefully, you don’t go through what I went through with the front-end developers to get here ?. Let’s go through what is happening.
We passed an anonymous function to exceptionFactory. The functions accept the array of validation errors. Diving into the validationError interface.
export interface ValidationError { target?: Record<string, any>; property: string; value?: any; constraints?: { [type: string]: string; }; children?: ValidationError[]; contexts?: { [type: string]: any; }; }
For example, if we apply IsEmail() on a field and the provided value is not valid. A validation error is created. We also want to know the property where the error occurred. We need to keep in mind that, we can have nested objects for example the photos in UserCreateDto and therefore we can have a parent property let’s say, photos where the error is with the url in the UserPhotoDto.
We first declare an inner function, that takes the errors and sets the parent property to an empty string since it is the root field.
const getValidationErrorsRecursively = ( validationErrors: ValidationError[], parentProperty = '', ) => { };
We then loop through the errors and get the property. For nested objects, I prefer to show the fields as photos.0.url. Where 0 is the index of the invalid photo in the array.
The error messages are stored in the constraints field as it’s in the validationError interface. We retrieve these errors and store them under a specific field.
if (error.constraints) { errors.push({ property: propertyPath, errors: Object.values(error.constraints), }); }
For nested objects, the children property of a validation error contains an array of validationError for the nested objects. We can easily get the errors by recursively calling our function and passing the parent property.
if (error.children?.length) { getValidationErrorsRecursively(error.children, propertyPath); }
Creating Custom Validators
While Class-validator provides a comprehensive set of built-in validators, there are times when your requirements exceed the standard validation rules or the standard validation doesn’t fit what you want to do. Custom validators are useful when you need to enforce rules that aren’t covered by the standard validators. Examples:
- We can create a custom validator to enforce a specific rule on what a valid password should be.
- We can create another to ensure that the username is unique.
To create a custom validator, we have to define a new class that implements the ValidatorConstraintInterface from class-validator. This requires us to implement two methods:
- validate → Contains your validation logic and must return a boolean
- defaultMessage → Optional default message to return when the validation fails.
Custom Password Validator
Create a new folder in users module named validators. Create two files, is-valid-password.validator.ts and is-username-unique.validator.ts. It should look like this.
A valid password in our use case is very simple. it should contains
- At least one uppercase letter.
- At least one lowercase letter.
- At least one symbol.
- At least one number.
- Password length should be more than 5 characters and less than 20 characters.
Update is-valid-password.validator.ts
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, } from 'class-validator'; @ValidatorConstraint({ name: 'IsStrongPassword', async: false }) export class IsValidPasswordConstraint implements ValidatorConstraintInterface { validate(password: string, args: ValidationArguments) { return ( typeof password === 'string' && password.length > 5 && password.length <= 20 && /[A-Z]/.test(password) && /[a-z]/.test(password) && /[0–9]/.test(password) && /[!@#$%^&*(),.?":{}|<>]/.test(password) ); } defaultMessage(args: ValidationArguments) { return 'Password must be between 6 and 20 characters long and include at least one uppercase letter, one lowercase letter, one number, and one special character'; } }
IsValidPasswordContraint is a custom validator because it is decorated with ValidatorConstraint(), we provide our custom validation rules in the validate method. If the validate function returns false, the error message in the defaultMessage will be returned. Providing these methods implements the ValidatorContraintInterface. To use isValidPasswordContraint, update the password field in UserCreateDto. For ValidatorConstraint({ name: ‘IsStrongPassword’, async: false }), we provided the constraint name that will be used to retrieve the error and also, since all actions in the validate are synchronous, we set async to false.
import { Validate } from 'class-validator'; export class UserCreateDto { // other fields @Validate(IsValidPasswordConstraint) public readonly password: string; }
Now, if we try again with an invalid password, we get this result indicating our custom validator is working.
We can go further and create a decorator for the validator so that we can decorate the password field without using the Validate.
Update is-valid-password.validator.ts
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, registerDecorator, ValidatorOptions, } from 'class-validator'; @ValidatorConstraint({ name: 'IsStrongPassword', async: false }) class IsValidPasswordConstraint implements ValidatorConstraintInterface { // removing the implementation so that we focus on IsPasswordValid function } export function IsValidPassword(validationOptions?: ValidatorOptions) { return function (object: NonNullable<unknown>, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsValidPasswordConstraint, }); }; }
Creating custom decorators makes working with validators a breeze, NestJs gives us registerDecorator to create our own. we provide it with the validator which is the IsValidPasswordContraint we created. We can use it like this
export class UserCreateDto { // other fields @IsValidPassword() public readonly password: string; }
Asynchronous Custom Validator With Custom Validation Options
It is common to encounter scenarios where you need to validate against external systems. Let’s assume that the username in UserCreateDto is unique across the various servers.
Update is-unique-username.validator.ts
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, registerDecorator, ValidationOptions, } from 'class-validator'; interface IsUsernameUniqueOptions { server: string; message?: string; } @ValidatorConstraint({ name: 'IsUsernameUnique', async: true }) export class IsUsernameUniqueConstraint implements ValidatorConstraintInterface { async validate(username: string, args: ValidationArguments) { const options = args.constraints[0] as IsUsernameUniqueOptions; const server = options.server; // server check, let assume username exist return !(await this.checkUsernameOnServer(username, server)); } defaultMessage(args: ValidationArguments) { const options = args?.constraints[0] as IsUsernameUniqueOptions; return options?.message || 'Username is already taken'; } async checkUsernameOnServer(username: string, server: string) { return true; } } export function IsUsernameUnique( options: IsUsernameUniqueOptions, validationOptions?: ValidationOptions,) { return function (object: object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [options], validator: IsUsernameUniqueConstraint, }); }; }
Usage
export class UserCreateDto { @IsString() @Length(2, 30, { message: 'Name must be between 2 and 30 characters' }) @Transform(({ value }) => value.trim()) @IsUsernameUnique({ server: 'east-1', message: 'Name already exists' }) public readonly name: string; // other fields }
We created a simple interface to show the possible options we can pass to the decorator. These options are constraints that will be used by IsUsernameUniqueConstraint, we can get them through the validation arguments . const options = args.constraints[0] as IsUsernameUniqueOptions;
Logging options give us { server: ‘east-1’, message: ‘Name already exists’ }, We then called the required service and passed the server name and username to validate the uniqueness of the name.
Also, async is set to true to allow asynchronous operations inside the validate function; ValidatorConstraint({ name: ‘IsUsernameUnique’, async: true }).
Common Pitfalls and Best Practices
It is necessary to be aware of common pitfalls to ensure robust and maintainable code.
- Avoid direct use of entities. One common mistake is using entities directly. Entities are typically used for database interactions and may contain fields or relationships that shouldn’t be exposed or validated on incoming requests.
- Test Custom Validators Extensively. Validation logic is a critical part of your application’s security and data integrity. Ensure they are well-tested.
- Be Explicit with Error Messages. Provide error messages that are informative and user-friendly. It should communicate what the user should do to correct it.
- Leverage Built-in and Custom Validators Together. Our IsUniqueUsername validator still uses IsString() on the name field. We don’t have to reinvent everything if it is already available.
Conclusion
There is so much to add like validation groups, using service containers, etc, but this article is getting way longer than I anticipated ?. As you continue developing with NestJS, I encourage you to explore more complex use cases and scenarios and share your experiences to keep the learning journey going.
Data validation is crucial in ensuring data integrity within any application and the principles covered here will serve as a strong foundation for further growth and mastery in building secure and efficient applications.
This is my very first article, and I’m eager to hear your thoughts! ? Please feel free to leave any feedback in the comments.
If you’d like to connect and stay updated on future content, you can find me on LinkedIn
Happy Coding !!!
Sumber Tambahan
- https://github.com/typestack/class-validator?tab=readme-ov-file#class-validator
- https://github.com/typestack/class-transformer?tab=readme-ov-file#what-is-class-transformer
- https://docs.nestjs.com/pipes
- https://docs.nestjs.com/techniques/validation
以上是掌握 NestJS 中的資料驗證:類別驗證器和類別轉換器的完整指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。
