发布于 2026-01-06 2 阅读
0

理解 TypeScript (NestJS) 中的数据传输对象 (DTO) 和数据验证

理解 TypeScript (NestJS) 中的数据传输对象 (DTO) 和数据验证

数据传输对象 (DTO) 是 NestJS 应用中数据验证的基础。DTO 为 NestJS 提供了多层次的灵活数据验证。

在本出版物中,您将深入了解数据传输对象,讨论验证机制、身份验证模型以及您需要了解有关数据传输对象的所有信息。

先决条件

本教程并非面向初学者。如果您刚开始使用 NestJS,请阅读这篇文章

要充分理解本文,您需要满足以下要求。

  • 具备使用 MongoDB 创建 NestJS 应用的经验。
  • 熟练使用 PostMan(推荐)或其他 API 测试工具。
  • 具备 bcrypt 或 NodeJS crypto 模块的基本知识。

一旦您满足这些要求,我们就可以开始了。

什么是数据传输对象?

数据传输对象(通常称为 DTO)是一种用于验证数据并定义发送到 Nest 应用程序的数据结构的对象。DTO 与接口类似,但与接口的区别在于以下几点:

  • 接口用于类型检查和结构定义。
  • DTO 用于类型检查、结构定义和数据验证。
  • 接口在编译过程中会消失,因为它是 TypeScript 的原生接口,而 JavaScript 中不存在接口。
  • DTO 使用原生 JavaScript 支持的类来定义,因此编译后仍然存在。

单独使用 DTO 只能进行类型检查和结构定义。要使用 DTO 运行数据验证,您需要使用 NestJS ValidationPipe

验证机制

NestJS 使用管道来验证数据。通过管道的数据会被评估,如果通过验证,则原样返回;否则,抛出错误。

NestJS 有 8 个内置管道,但您将重点关注ValidationPipe使用该class-validator包的管道,因为它抽象了很多冗长的代码,并且可以使用装饰器轻松验证数据。

简单用户身份验证模型中的 DTO

用户身份验证模型是揭开数据转移对象 (DTO) 神秘面纱的绝佳例子。这是因为它需要多层数据验证。让我们创建一个这样的模型,并探索 DTO 以及如何ValidationPipe验证所有进入我们应用程序的数据形式。

搭建开发环境

设置开发环境:

  • 使用 Nest CLI 创建一个新项目,
  • 使用 CLI生成您的ModuleService、 ,Controller
  • 安装Mongoose应用程序并将其连接到数据库,
  • 创建您的模式文件夹并定义Schema

典型的用户认证模式如下所示:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop({ required: true })
  fullName: string;

  @Prop({ required: true })
  email: string;

  @Prop({ required: true })
  password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Enter fullscreen mode Exit fullscreen mode

具有fullName、、emailpassword作为必需属性。

  • 在你的模块内创建一个名为 `.dto` 的文件夹dto。你的 DTO 类将存储并导出到这里。
  • 在文件夹内创建一个文件dto,并将其命名为user.dto.ts

DTO的结构

DTO 是一种类,因此它遵循与类相同的语法。

export class newUserDto {
  fullName: string;
  email: string;
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

以上是基本 DTO 的结构。

这种结构仅用于类型检查,不具备数据验证功能。

实施数据验证

要添加验证功能,请按照以下步骤操作;

  • 在你里面main.ts
  • {ValidationPipe}从以下来源导入'@nestjs/common'
  • 在函数内部bootstrap,紧挨着常量下方app,调用该useGlobalPipes方法并将其作为参数app传递。new ValidationPipe()
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as dotenv from 'dotenv';
dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

这将启用自动验证,并确保所有端点免受错误数据的侵害。验证管道接受一个选项对象作为参数,您可以在官方文档中找到更多相关信息。

  • 运行class-validator以下命令安装该软件包:class-transformer
npm i --save class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode

该函数class-transformer可以将 JSON 对象转换为 DTO 类的实例,反之亦然。

class-validator软件包包含大量验证查询,您可以在其文档中找到它们,但您将重点关注其中几个与用户数据验证相关的查询。在您的user.dto.ts文件中,从以下位置导入查询class-validator

  • IsNotEmpty此验证器检查给定值是否为空。它接受一个选项对象作为参数。
  • IsString此验证器检查给定值是否为真正的字符串。它接受一个选项对象作为参数。
  • IsEmail此验证器检查给定值是否为电子邮件类型。它接受一个选项对象作为参数。
  • Length此验证器接受 3 个参数。第一个参数是minLength,第二个参数是maxLength,最后一个参数是一个选项对象。

以上代码将用作装饰器来验证各自的字段。请在您的 DTO 中实现此功能。

import { IsNotEmpty, IsEmail, Length, IsString } from 'class-validator';

export class newUserDto {
  @IsNotEmpty({ message: 'Please Enter Full Name' })
  @IsString({ message: 'Please Enter Valid Name' })
  fullName: string;

  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  @Length(6, 50, {
    message: 'Password length Must be between 6 and 50 charcters',
  })
  password: string;
}

Enter fullscreen mode Exit fullscreen mode

密码字段目前只有Length验证,但在实际生产环境中,您可能需要添加一些其他字段以确保密码更加安全。

这些新的验证字段可能包括检查密码是否包含大写字母、小写字母和数字。

这可以通过多种方式实现,包括:

  • 添加更多验证装饰器,就像你在fullName字段中所做的那样。
  • 使用@Matches验证器并将检查您的要求的正则表达式作为参数传递。

但最简洁的方法是创建一个自定义验证器来满足您的需求。您可以在这里了解更多关于自定义装饰器的信息。

发布和提交请求

POST通过向应用程序发送少量请求来测试数据验证PUT。但在此之前,以明文形式存储密码是一种糟糕的做法。因此,在将密码存储到数据库之前,请先对其进行哈希处理;

密码哈希

您可以使用名为 `crypto` 的包bcrypt或 NodeJS 的 `crypto` 模块对密码进行哈希处理。本文档涵盖了 `crypto` 的相关内容bcrypt。要安装,bcrypt请运行 `mash`。

npm i bcrypt
npm i -D @types/bcrypt
Enter fullscreen mode Exit fullscreen mode

创建一个utils文件夹来存储包含实用函数的文件,例如用于对密码进行哈希处理的函数。

为了确保用户密码的安全,需要在将其存储到数据库之前对其进行哈希处理。让我们来实现这一点。

  • 在你的文件夹中创建一个文件utils,该文件将包含你的实用函数。
  • 导入*方式bcrypt'bcrypt'
  • 创建一个函数,该函数接收原始密码并对其进行哈希处理,bcrypt然后返回哈希后的密码。导出该函数。
import * as bcrypt from 'bcrypt';

export async function hashPassword(textPassword: string) {
  const salt = await bcrypt.genSalt();
  const hash = await bcrypt.hash(textPassword, salt);
  return hash;
}
Enter fullscreen mode Exit fullscreen mode

该软件包有多种使用方法bcrypt,您可以在这里找到。

现在你已经有了对密码进行哈希处理的函数,请实现你的第一个POST请求。

创建新用户

要创建具有有效凭据的新用户,您需要将之前创建的 DTO 导入到您的服务中。

服务逻辑

  • 创建一个async函数newUser,该函数接收一个参数user,该参数应为新用户的数据。将其类型设置为之前创建的 DTO。
  • 导入该hashPassword函数。
  • 在函数内部async,创建一个常量,并将等待函数password返回的值作为参数赋值给该常量。hashPassworduser.password
  • 返回您自己的版本的 my 的新实例userModel作为参数传入一个包含解构user和的对象password,并对其调用该save()方法。
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { newUserDto } from './dto/user.dto';
import { hashPassword } from './utilis/bcrypt.utils';

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name)
    private userModel: Model<UserDocument>,
  ) {}

  async newUser(user: newUserDto): Promise<User> {
    const password = await hashPassword(user.password);
    return await new this.userModel({ ...user, password }).save();
  }
}
Enter fullscreen mode Exit fullscreen mode

我们之前设置的 DTO 和验证器会不断检查数据是否有效,然后再将用户数据存储到数据库中。

实现控制器逻辑,以便您可以使用一些虚拟数据测试 DTO。

控制器逻辑

import {
  Controller,
  Body,
  Post, 
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { newUserDto } from './dto/user.dto';

@Controller('users')
export class AuthController {
  constructor(private readonly service: AuthService) {}

  @Post('signup')
  async createUser(
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return await this.service.newUser(user);
  }
Enter fullscreen mode Exit fullscreen mode

如上面的代码块所示,该user参数的类型为newUserDto。这确保了所有进入应用程序的数据都与 DTO 匹配,否则会抛出错误。

测试端点

http://localhost:3000/users/signup使用 Postman 或您喜欢的测试工具,通过发送请求,用一些虚拟数据测试验证结果。

{
    "fullName":"Jon Snow",
    "email":"snow@housestark.com",
    "password":"password"
}
Enter fullscreen mode Exit fullscreen mode

上面的 JSON 数据符合所有验证条件。因此,您将收到201包含数据、一个_id属性和一个哈希密码的响应。请复制该_id属性,在测试请求中的数据验证时需要用到它PUT

{
    "fullName":"Arya Stark",
    "email":"aryathefaceless@housestark",
    "password":"password"
}
Enter fullscreen mode Exit fullscreen mode

上面的 JSON 数据包含一个无效email属性。因此,您将收到一条包含错误描述消息的响应。请将该属性400更新为 . 以将其保存到数据库中emailaryathefaceless@housestark.com

{
    "fullName":"Tyrion Lannister",
    "email":"tyrion@houselannister.com",
    "password":"pass",
     "house":"Lannister"
}
Enter fullscreen mode Exit fullscreen mode

上面的 JSON 数据存在 2 个问题,密码太短(少于 6 个字符),并且多了一个字段house

增加请求长度password并再次发送请求。您将收到200包含已处理数据的请求,但该house字段不会存储在数据库中,因为它在通过验证管道时已被过滤。

更新用户

实现PUT按需更新用户数据的路由。

服务逻辑

async updateUser(id: string, userData: newUserDto): Promise<User> {
    return await this.userModel.findByIdAndUpdate(id, userData);
  }
Enter fullscreen mode Exit fullscreen mode

控制器逻辑

@Put(':id')
  async updateuser(
    @Param('id')
    id: string,
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return this.service.updateUser(id, user);
  }
Enter fullscreen mode Exit fullscreen mode

与请求类似POST,数据user也被赋予了类型newUserDto。因此,所有数据在保存到数据库之前都会经过彻底检查。

通过更新数据库中存储的某个用户来测试此端点。请记住,您复制的是_id属于 Jon Snow 的数据。

所以,请使用以下数据PUT发出请求;http://localhost:3000/id

{
    "fullName":"Jon Snow",
    "email":"snow@housetargaryen.com",
    "password":"password"
}
Enter fullscreen mode Exit fullscreen mode

以上数据符合所有验证条件,因此会返回200响应。如果任何数据字段包含无效数据,则会返回400响应,并且PUT请求会失败。

用户登录

如果无法实现用户登录,用户身份验证模型就不完整。要实现这一逻辑;

首先,你需要一个bcrypt实用函数来比较哈希密码和明文密码。像这样:

export async function validatePassword(textPassword: string, hash: string) {
  const validUser = await bcrypt.compare(textPassword, hash);
  return validUser;
}
Enter fullscreen mode Exit fullscreen mode

接下来,您需要为登录数据创建一个新的 DTO。该 DTO 与之前的 DTO 类似,newUserDto但没有该fullName属性。

import { IsEmail } from 'class-validator';

export class loginUserDto {
  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  password: string;
}
Enter fullscreen mode Exit fullscreen mode

请注意,该属性未添加验证器password。这是因为这样做可能会在未来造成安全威胁,因为它会将密码长度范围暴露给恶意用户。

服务逻辑

async loginUser(loginData: loginUserDto) {
    const email = loginData.email;
    const user = await this.userModel.findOne({ email });
    if (user) {
      const valid = await validatePassword(loginData.password, user.password);
      if (valid) {
        return user;
      }
    }
    return new UnauthorizedException('Invalid Credentials');
  }
Enter fullscreen mode Exit fullscreen mode

您的服务逻辑应该能够做到:

检查用户是否存在;

  • 如果用户存在,则验证密码。
  • 如果用户不存在,则抛出异常。

验证密码;

  • 如果密码匹配,则返回用户
  • 如果密码不匹配,则抛出异常。

控制器逻辑

@Post('login')
  async loginUser(
    @Body()
    loginData: loginUserDto,
  ) {
    return await this.service.loginUser(loginData);
  }
Enter fullscreen mode Exit fullscreen mode

你的控制器应该向……发出POST请求http://localhost:3000/users/login

结论

本文到此结束。以下是文章内容的总结。

  • 什么是DTO?
  • DTO 和接口之间的区别
  • DTO的结构,
  • NestJS验证机制
  • 使用 bcrypt 创建一个简单的用户身份验证模型。

那真是相当多了,恭喜你走到这一步。

你可以在Github上找到代码

祝您编程愉快!

文章来源:https://dev.to/davidekete/understanding-data-transfer-objects-dto-and-data-validation-in-typescript-nestjs-1j2b