DTO 参数校验和查询结果过滤
以用户为例:插入修改时校验接口参数、查询时过滤需要隐藏字段
安装插件
shell
pnpm install class-validator class-transformerDTO 参数校验
DTO:数据传输对象 :::tip 特点:全局管道校验所有 POST、PUT 接口的入参,所以每一个接口都需要定义 DTO,否则会被校验拦截 :::
1、注册全局管道
ts
// src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { ClassSerializerInterceptor } from '@nestjs/common/serializer';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局管道
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 自动删除非白名单属性
forbidNonWhitelisted: false, // 如果有非白名单属性,true 抛出错误,false 不抛出错误
transform: true, // 自动类型转换,如字符串数字转 number
// transformOptions: {
// enableImplicitConversion: true, // 隐式类型转换(如 '18' 转 18),此属性慎用:空字符会转为 0 会意外跳过为空验证
// },
disableErrorMessages: process.env.NODE_ENV === 'production', // 生产环境可关闭错误详情
})
);
app.setGlobalPrefix('api/v1');
await app.listen(3000);
}
bootstrap();2、定义 DTO
- 创建用户接口的 DTO
ts
// src/user/dto/create-user.dto.ts
// src/user/dto/create-user.dto.ts
import {
IsString,
IsEmail,
IsNotEmpty,
MinLength,
IsOptional,
IsEnum,
Matches,
} from 'class-validator';
import { UserStatus, UserRole } from '../../common/enums/user.enum';
export class CreateUserDto {
/**
* 用户名
* @example "zhangsan"
*/
@IsString({ message: '用户名必须为字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
username: string;
/**
* 密码(最小6位,包含字母+数字)
* @example "Zhang123"
*/
@IsString({ message: '密码必须为字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@MinLength(6, { message: '密码长度不能少于6位' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, {
message: '密码必须包含字母和数字',
})
password: string;
/**
* 邮箱
* @example "zhangsan@example.com"
*/
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
/**
* 手机号(可选,正则校验中国大陆手机号)
* @example "13800138000"
*/
@IsOptional()
@Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
phone?: string;
/**
* 角色(可选,默认普通用户)
* @example "user"
*/
@IsOptional()
@IsEnum(UserRole, { message: `角色只能是${Object.values(UserRole).join('/')}` })
role: UserRole = UserRole.USER; // 默认值
/**
* 状态(可选,默认未激活)
* @example "inactive"
*/
@IsOptional()
@IsEnum(UserStatus, { message: `状态只能是${Object.values(UserStatus).join('/')}` })
status: UserStatus = UserStatus.INACTIVE; // 默认值
/**
* 备注(可选)
* @example "测试用户"
*/
@IsOptional()
@IsString({ message: '备注必须为字符串' })
remark?: string;
}- 修改用户接口的 DTO
ts
// src/user/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { IsOptional, IsString, MinLength, Matches } from 'class-validator';
// PartialType: 继承CreateUserDto并将所有字段设为可选
export class UpdateUserDto extends PartialType(CreateUserDto) {
// 覆盖密码规则:修改时密码可选,且保留格式校验
@IsOptional()
@IsString({ message: '密码必须为字符串' })
@MinLength(6, { message: '密码长度不能少于6位' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, {
message: '密码必须包含字母和数字',
})
password?: string;
}- 定义枚举数据
ts
// src/common/enums/user.enum.ts
export enum UserStatus {
ACTIVE = 'active', // 激活
INACTIVE = 'inactive', // 未激活
LOCKED = 'locked', // 锁定
}
export enum UserRole {
ADMIN = 'admin', // 管理员
USER = 'user', // 普通用户
OPERATOR = 'operator', // 运营
}3、使用 DTO
ts
// src/user/user.controller.ts
import { Controller, Delete, Get, Param, Post, Body, Put } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('user')
export class UserController {
// 新增用户
@Post()
create(@Body() createUserDto: CreateUserDto) {
// 入参已通过DTO校验,可直接使用
return { message: '用户创建成功', data: createUserDto };
}
// 修改用户(/:id 为用户ID,从URL获取)
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return { message: `用户${id}修改成功`, data: updateUserDto };
}
}4、测试
shell
# 接口
http://localhost:3000/api/v1/user
# 方法
POST
# 参数
{
"username": "",
"password": "1243"
}
# 结果
{
"message": [
"用户名不能为空",
"password must be longer than or equal to 6 characters"
],
"error": "Bad Request",
"statusCode": 400
}查询结果过滤
特点:全局拦截器过滤 GET 接口的返回值,主要是隐藏字段,只隐藏 @Exclude() 标记的字段,当前不对数据结构进行统一处理,只是过滤字段 :::
1、全局拦截器
ts
// 全局注册拦截器(需传入Reflector)(所有的接口参数都必须传入 DTO )
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector), {
excludeExtraneousValues: false, // 核心:只保留类中定义的字段,让@Exclude生效
})
);2、定义拦截规则
整改 entity 文件
ts
// src/user/user.entity.ts
import { Log } from 'src/log/log.entity';
import { Profile } from 'src/profile/profile.entity';
import { Roles } from 'src/roles/roles.entity';
import {
Column,
Entity,
JoinTable,
ManyToMany,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
VersionColumn,
} from 'typeorm';
import { Exclude } from 'class-transformer';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column({ select: false }) // 隐藏字段方案一
password: string;
@CreateDateColumn({ type: 'timestamp' })
@Exclude() // 隐藏字段方案二
createdAt: string;
@UpdateDateColumn()
@Exclude()
updatedAt: Date;
@DeleteDateColumn()
@Exclude()
deletedAt: Date;
@VersionColumn()
version?: number;
// 表示 user 表与 profile 表建立一对一关系
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
// 表示 user 表与 log 表建立一对多关系
@OneToMany(() => Log, (logs) => logs.user)
logs: Log[];
// 表示 user 表与 roles 表建立多对多关系,并创建中间表 users_roles
@ManyToMany(() => Roles, (roles) => roles.user)
@JoinTable({
name: 'users_roles', // 自定义连接表名
joinColumn: {
name: 'user_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'role_id',
referencedColumnName: 'id',
},
})
roles: Roles[];
}4、测试
shell
# 接口
http://localhost:3000/api/v1/user
# 方法
GET
# 参数
无
# 结果
[
{
"id": 2,
"username": "lisi",
"version": 1
},
{
"id": 3,
"username": "alias",
"version": 1
}
]注意
- 使用 @Request() 方法获取 req 参数,无法拦截 dto 校验,只能使用 @Body()/@Query()/@Param() 等参数,即:只能校验 req 内部的属性,Nest 不会处理整个 req 对象的校验
- 如果要给 req 参数校验,建议:login(@Body() LoginReqDto: LoginReqDto, @Request() req: any),使用其内部的@Body()等参数校验,然后在后面使用 req 进行挂载
