跳到主要内容

DTO 数据传输对象

DTO是"数据传输对象"(Data Transfer Object)的缩写,它是一种设计模式,通常用于应用程序的不同层之间传输数据。

简单概念入门

如果没有 DTO 就需要在 controller 中重复写 @Rep() 等等的代码

import { Controller, Post, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Request } from 'express';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('signup')
  signup(@Req() req: Request) {
    console.log(req.body);
    return this.authService.signup();
  }
}

创建 DTO 文件

module 文件夹下创建 dto

./src/auth
├── auth.controller.ts
├── auth.module.ts
├── auth.service.ts
└── dto
├── auth.dto.ts
└── index.ts

auth.dto.ts

export interface AuthDto {
  email: string;
  password: string;
}

index.ts

export * from './auth.dto';
提示

建议在 typescript 中的文件模块使用 index.ts 来导出子模块

controller 中使用

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

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('signin')
  signin(@Body() dto: AuthDto) {
    console.log({ dto });
    return this.authService.signin();
  }
}

使用 pipes 校验数据

  • dto
  • 使用 pipes 检验字段

一、单字段检验

nest pipes

代码示例

// controller
{
...
  @Post('signin')
  signin2(
    @Body('email') email: string,
    @Body('password') password: string,
    @Body('age', ParseIntPipe) age: number,
  ) {
    console.log({
      email,
      typeOfEmail: typeof email,
      password,
      typeOfPassword: typeof password,
      age,
      typeOfAge: typeof age,
    });
    return this.authService.signin();
  }
}
// http://localhost:3333/auth/signin
{
    "email": "306624995@qq.com",
    "password": "test",
    "age": 18
}

存入正确信息后,控制台输出

{
email: '306624995@qq.com',
typeOfEmail: 'string',
password: 'test',
typeOfPassword: 'string',
age: 18,
typeOfAge: 'number'
}

输入年龄错误时,则不继续逻辑运算,直接返回错误信息给客户端

{
    "message": "Validation failed (numeric string is expected)",
    "error": "Bad Request",
    "statusCode": 400
}

二、使用 Class validator

npm i --save class-validator class-transformer
// dto 文件
import { IsEmail, IsString, IsNotEmpty } from 'class-validator';

export class AuthDto {
@IsEmail()
@IsNotEmpty()
email: string

@IsString()
@IsNotEmpty()
password: string
}

在全局注册 ValidationPipe

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3333);
}
bootstrap();

测试效果

{
    "email": "",
    "password": 1
}
{
    "message": [
        "email should not be empty",
        "email must be an email",
        "password must be a string"
    ],
    "error": "Bad Request",
    "statusCode": 400
}
注意

存在问题,假如入参多传参数。只需要在 ValidationPipe 实例化时加上{whitelist: true} 即可

...
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true
  }));
  await app.listen(3333);
}
bootstrap();

密码校验

// https://github.com/ranisalt/node-argon2
pnpm i argon2
  • 主要作用创建 hash 密码,并保存在数据库中
// auth.service.ts
export class AuthService {
  constructor(private prisma: PrismaService) {}
  async signup(dto: AuthDto) {
    // generate the password hash value
    const hash = await argon.hash(dto.password);

    // save the new user in the db
    const user = await this.prisma.user.create({
      data: {
        email: dto.email,
        hash,
      },
      // 1. Select return Object
      // select: {
      //   email:true,
      //   hash: false,
      //   ...
      // },
    });

    // 2. The hash of user is not returned
    delete user.hash;

    return user;
  }
  ...
}

数据库联表

将 user 表的 id 与 bookmark 中进行关联

model User {
  id       Int      @id @default(autoincrement())
  createAt DateTime @default(now())
  updateAt DateTime @updatedAt

  email String @unique
  hash  String

  firstName String?
  lastName  String?

  bookmarks Bookmark[]
 
  @@map("users")
}

model Bookmark {
  id       Int      @id @default(autoincrement())
  createAt DateTime @default(now())
  updateAt DateTime @updatedAt

  title       String
  link        String
  description String?

  userId Int
  user User @relation(fields: [userId], references: [id])

  @@map("bookmarks")
}
npx prisma migrate dev

prisma 错误捕获

由于 prisma 中的 email 字段添加 @unique 的字段,重复注册将报错。

控制台报错

Unique constraint failed on the fields: (`email`)

客服端返回

{
    "statusCode": 500,
    "message": "Internal server error"
}
信息

使用 try catch 捕获,详细错误码可点击 check prisma error-codes

// auth.service.ts
export class AuthService {
  constructor(private prisma: PrismaService) {}
  async signup(dto: AuthDto) {
    // generate the password hash value
    const hash = await argon.hash(dto.password);

    try {
      // save the new user in the db
      const user = await this.prisma.user.create({
        data: {
          email: dto.email,
          hash,
        }
      });

      delete user.hash;
      return user;
     
    } catch (error) {
      if (error instanceof PrismaClientKnownRequestError) {
        if (error.code === 'P2002') {
          throw new ForbiddenException(
            `Unique consstraint failed on the ${error.meta?.target}`,
          );
        }
      }
      throw error;
    }
  }
  ...
}