NestJS新增功能与DTO数据校验实战
NestJS新增功能与DTO数据校验实战
学习目标:掌握新增功能的完整实现流程、熟练创建 DTO 并使用 class-validator 进行数据校验、理解 ValidationPipe 在新增功能中的作用。
一、新增功能完整流程
1.1 新增功能的完整流程
新增功能完整流程:
│
├── 第一步:前端发送请求
│ └── POST /home { "title": "xxx", "url": "xxx", ... }
│
├── 第二步:ValidationPipe 校验数据
│ ├── class-transformer 转换数据
│ ├── class-validator 校验数据
│ └── 校验失败 → 返回错误
│
├── 第三步:Controller 接收请求
│ ├── 使用 @Body() 获取请求体
│ └── 自动校验并转换为 DTO 实例
│
├── 第四步:Service 处理业务逻辑
│ ├── 接收 DTO 实例
│ └── 调用 Prisma 创建数据
│
├── 第五步:Prisma 操作数据库
│ └── prisma.homeResources.create({ data: dto })
│
├── 第六步:数据库返回结果
│ └── 返回新创建的记录
│
└── 第七步:响应给前端
└── { "id": 1, "title": "xxx", ... }1.2 新增功能中的数据校验
新增功能中的数据校验流程:
│
├── 用户提交数据
│ └── 可能包含:错误类型、恶意字段、缺失字段
│
├── ValidationPipe 自动校验
│ ├── 类型校验:确保数据类型正确
│ ├── 格式校验:确保数据格式正确(如 URL、Email)
│ ├── 必填校验:确保必填字段不为空
│ ├── 枚举校验:确保值在允许范围内
│ └── 恶意字段过滤:自动过滤未定义字段
│
├── 校验成功
│ ├── 数据安全,可以写入数据库
│ └── 避免脏数据污染数据库
│
└── 校验失败
├── 立即返回错误信息
├── 不进入 Controller
├── 不执行业务逻辑
└── 不访问数据库(性能优化)二、安装依赖与配置
2.1 安装依赖
# 安装 class-validator 和 class-transformer
$ pnpm add class-validator class-transformer依赖说明:
| 依赖 | 作用 | 说明 |
|---|---|---|
class-validator | 数据校验 | 提供装饰器校验数据类型和格式 |
class-transformer | 数据转换 | JSON 对象转换为 Class 实例 |
2.2 配置全局 ValidationPipe
在 main.ts 中配置:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 配置全局 ValidationPipe
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 自动类型转换
whitelist: true, // 启用白名单
forbidNonWhitelisted: true, // 拒绝未定义的字段
stopAtFirstError: false, // 返回所有错误
disableErrorMessages: false, // 显示详细错误信息
}),
);
// 设置默认版本
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1', // 设置默认版本为 v1
});
await app.listen(3000);
}
bootstrap();配置说明:
transform: true:自动将请求参数转换为 DTO 类型whitelist: true:自动过滤未在 DTO 中定义的字段forbidNonWhitelisted: true:拒绝包含未定义字段的请求defaultVersion: '1':设置默认版本,无需在 URL 中指定版本
三、创建 DTO(数据传输对象)
3.1 DTO 文件命名规范
DTO 文件命名规范:
│
├── 创建 DTO
│ ├── 文件名:create-home-resource.dto.ts
│ ├── 类名:CreateHomeResourceDto
│ └── 作用:定义创建时的数据校验规则
│
├── 更新 DTO
│ ├── 文件名:update-home-resource.dto.ts
│ ├── 类名:UpdateHomeResourceDto
│ └── 作用:定义更新时的数据校验规则
│
├── 查询 DTO
│ ├── 文件名:query-home-resource.dto.ts
│ ├── 类名:QueryHomeResourceDto
│ └── 作用:定义查询参数的校验规则
│
└── 文件位置
├── src/modules/home/dto/create-home-resource.dto.ts
├── src/modules/home/dto/update-home-resource.dto.ts
└── src/modules/home/dto/query-home-resource.dto.ts3.2 对照 Schema 创建 DTO
Schema 定义(prisma/schema.prisma):
model HomeResources {
id Int @id @default(autoincrement())
title String?
subtitle String?
url String?
image String?
desc String?
module String @default("home")
type String?
icon String?
order Int @default(100)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("home_resources")
}CreateHomeResourceDto 定义:
// src/modules/home/dto/create-home-resource.dto.ts
import {
IsString,
IsOptional,
IsUrl,
IsInt,
IsIn,
Min,
} from 'class-validator';
export class CreateHomeResourceDto {
// 可选字符串字段
@IsOptional()
@IsString({ message: 'title 必须是字符串' })
title?: string;
@IsOptional()
@IsString({ message: 'subtitle 必须是字符串' })
subtitle?: string;
// URL 字段(需要格式校验)
@IsOptional()
@IsUrl({}, { message: 'url 格式不正确' })
url?: string;
@IsOptional()
@IsString({ message: 'image 必须是字符串' })
image?: string;
@IsOptional()
@IsString({ message: 'desc 必须是字符串' })
desc?: string;
// 枚举字段(必须是指定的值)
@IsIn(['home', 'study'], { message: 'module 必须是 home 或 study' })
module: string;
@IsOptional()
@IsString({ message: 'type 必须是字符串' })
type?: string;
@IsOptional()
@IsString({ message: 'icon 必须是字符串' })
icon?: string;
// 数字字段(有最小值限制)
@IsOptional()
@IsInt({ message: 'order 必须是整数' })
@Min(0, { message: 'order 不能小于 0' })
order?: number;
}3.3 DTO 装饰器详解
装饰器分类:
DTO 装饰器分类:
│
├── 1. 类型校验装饰器
│ ├── @IsString() # 校验字符串
│ ├── @IsInt() # 校验整数
│ ├── @IsBoolean() # 校验布尔值
│ ├── @IsNumber() # 校验数字
│ └── @IsArray() # 校验数组
│
├── 2. 可选字段装饰器
│ └── @IsOptional() # 标记为可选字段
│
├── 3. 格式校验装饰器
│ ├── @IsUrl() # 校验 URL 格式
│ ├── @IsEmail() # 校验邮箱格式
│ ├── @IsUUID() # 校验 UUID 格式
│ └── @IsDateString() # 校验日期字符串
│
├── 4. 枚举校验装饰器
│ └── @IsIn([]) # 校验值是否在列表中
│
├── 5. 数值范围装饰器
│ ├── @Min() # 最小值
│ └── @Max() # 最大值
│
└── 6. 长度限制装饰器
├── @MinLength() # 最小长度
└── @MaxLength() # 最大长度装饰器使用示例:
export class CreateHomeResourceDto {
// 可选字符串
@IsOptional()
@IsString({ message: 'title 必须是字符串' })
title?: string;
// URL 格式校验
@IsOptional()
@IsUrl({}, { message: 'url 格式不正确' })
url?: string;
// 枚举值校验
@IsIn(['home', 'study'], { message: 'module 必须是 home 或 study' })
module: string;
// 数字范围校验
@IsOptional()
@IsInt({ message: 'order 必须是整数' })
@Min(0, { message: 'order 不能小于 0' })
order?: number;
}3.4 DTO 与 Schema 的对应关系
| Schema 字段 | 类型 | 默认值 | DTO 字段 | 装饰器 |
|---|---|---|---|---|
title | String? | - | title?: string | @IsOptional() @IsString() |
subtitle | String? | - | subtitle?: string | @IsOptional() @IsString() |
url | String? | - | url?: string | @IsOptional() @IsUrl() |
image | String? | - | image?: string | @IsOptional() @IsString() |
desc | String? | - | desc?: string | @IsOptional() @IsString() |
module | String | "home" | module: string | @IsIn(['home', 'study']) |
type | String? | - | type?: string | @IsOptional() @IsString() |
icon | String? | - | icon?: string | @IsOptional() @IsString() |
order | Int | 100 | order?: number | @IsOptional() @IsInt() @Min(0) |
重要说明:
- Schema 中的
?表示可选字段,DTO 中使用@IsOptional() - Schema 中的
@default()表示有默认值,DTO 中可以使用@IsOptional() - Schema 中的
String对应 TypeScript 的string - Schema 中的
Int对应 TypeScript 的number
四、Controller 实现新增功能
4.1 使用 @Body 接收请求体
// src/modules/home/home.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { HomeService } from './home.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
@Controller('home')
export class HomeController {
constructor(private readonly homeService: HomeService) {}
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
// createDto 已经经过 ValidationPipe 校验和转换
// createDto 是 CreateHomeResourceDto 的实例
return this.homeService.create(createDto);
}
}关键点说明:
@Body()装饰器:获取请求体数据createDto: CreateHomeResourceDto:指定 DTO 类型- ValidationPipe 自动校验并转换数据
- 校验失败会自动返回错误,不进入 Controller
4.2 ValidationPipe 自动校验流程
ValidationPipe 自动校验流程:
│
├── 第一步:接收请求体
│ └── { "title": 123, "url": "invalid", "module": "other" }
│
├── 第二步:class-transformer 转换
│ ├── 将 JSON 对象转换为 CreateHomeResourceDto 实例
│ └── const dto = plainToInstance(CreateHomeResourceDto, body)
│
├── 第三步:class-validator 校验
│ ├── 检查每个属性的装饰器规则
│ ├── title 必须是字符串 → 失败
│ ├── url 必须是 URL 格式 → 失败
│ └── module 必须是 home 或 study → 失败
│
└── 第四步:返回结果
├── 校验失败 → 返回错误信息
└── 校验成功 → 进入 Controller 方法4.3 Controller 与 Service 的协作
// Controller 层
@Controller('home')
export class HomeController {
constructor(private readonly homeService: HomeService) {}
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
// 1. 接收校验后的数据
// 2. 调用 Service 处理业务逻辑
// 3. 返回 Service 的结果
return this.homeService.create(createDto);
}
}
// Service 层
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async create(createDto: CreateHomeResourceDto) {
// 1. 接收 Controller 传递的数据
// 2. 调用 Prisma 操作数据库
// 3. 返回创建的记录
return this.prisma.homeResources.create({
data: createDto,
});
}
}五、Service 实现新增功能
5.1 Service 层实现
// src/modules/home/home.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async create(createDto: CreateHomeResourceDto) {
// 使用 Prisma 创建数据
return this.prisma.homeResources.create({
data: createDto,
});
}
}5.2 Prisma create 方法详解
// Prisma create() 方法签名
prisma.model.create({
data: {
// 字段名: 值
},
select?: { // 可选:选择返回的字段
id: true,
title: true,
},
include?: { // 可选:包含关联数据
user: true,
},
})
// 示例
const resource = await this.prisma.homeResources.create({
data: {
title: '新资源',
url: 'https://example.com',
module: 'home',
order: 100,
},
});
// 返回值:创建的记录对象
{
id: 1,
title: '新资源',
url: 'https://example.com',
module: 'home',
order: 100,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
}六、数据校验实战测试
6.1 测试场景一:类型错误
测试数据:
{
"title": 123, // 应该是字符串
"module": "home"
}响应结果:
{
"statusCode": 400,
"message": [
"title 必须是字符串"
],
"error": "Bad Request"
}说明:
- title 应该是 string 类型,但传入了 number
- ValidationPipe 自动拦截错误
- 不会进入 Controller 方法
- 不会访问数据库
6.2 测试场景二:格式错误
测试数据:
{
"url": "invalid-url", // 不是有效的 URL
"module": "home"
}响应结果:
{
"statusCode": 400,
"message": [
"url 格式不正确"
],
"error": "Bad Request"
}正确的 URL 格式:
{
"url": "https://example.com", // 正确的 URL 格式
"module": "home"
}6.3 测试场景三:枚举值错误
测试数据:
{
"module": "other" // 不在允许的枚举值中
}响应结果:
{
"statusCode": 400,
"message": [
"module 必须是 home 或 study"
],
"error": "Bad Request"
}正确的枚举值:
{
"module": "home" // 正确的枚举值
}或
{
"module": "study" // 正确的枚举值
}6.4 测试场景四:数字范围错误
测试数据:
{
"module": "home",
"order": -1 // 不能小于 0
}响应结果:
{
"statusCode": 400,
"message": [
"order 不能小于 0"
],
"error": "Bad Request"
}正确的数字范围:
{
"module": "home",
"order": 100 // 正确的数字范围
}6.5 测试场景五:校验成功
测试数据:
{
"title": "新资源",
"subtitle": "副标题",
"url": "https://example.com",
"image": "https://example.com/image.jpg",
"desc": "资源描述",
"module": "home",
"type": "project",
"icon": "icon-name",
"order": 100
}响应结果:
{
"id": 1,
"title": "新资源",
"subtitle": "副标题",
"url": "https://example.com",
"image": "https://example.com/image.jpg",
"desc": "资源描述",
"module": "home",
"type": "project",
"icon": "icon-name",
"order": 100,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}说明:
- 所有字段都通过校验
- 数据成功写入数据库
- 返回创建的记录
七、使用 DTO 的优势
7.1 传统方式 vs DTO 方式
** 传统方式(不推荐)**:
@Post()
async create(@Body() body: any) {
// 手动校验每个字段
if (!body.title || typeof body.title !== 'string') {
throw new BadRequestException('title 必须是字符串');
}
if (body.url && !this.isValidUrl(body.url)) {
throw new BadRequestException('url 格式不正确');
}
if (!['home', 'study'].includes(body.module)) {
throw new BadRequestException('module 必须是 home 或 study');
}
if (body.order !== undefined) {
if (typeof body.order !== 'number' || body.order < 0) {
throw new BadRequestException('order 必须是非负整数');
}
}
// ... 更多校验逻辑
// 创建数据
return this.homeService.create(body);
}** DTO 方式(推荐)**:
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
// 自动校验,无需手动编写校验逻辑
return this.homeService.create(createDto);
}对比总结:
| 维度 | 传统方式 | DTO 方式 |
|---|---|---|
| 代码量 | 大量 if-else | 装饰器声明 |
| 可读性 | 差 | 好 |
| 可维护性 | 难以维护 | 易于维护 |
| 复用性 | 无法复用 | 可复用 |
| 错误处理 | 手动抛出异常 | 自动返回错误 |
| 类型安全 | any 类型 | 强类型 |
7.2 DTO 的优势总结
DTO 的优势:
│
├── 1. 代码简洁
│ ├── 无需手动编写校验逻辑
│ ├── 使用装饰器声明规则
│ └── 自动校验和转换
│
├── 2. 类型安全
│ ├── TypeScript 强类型
│ ├── IDE 自动提示
│ └── 编译时类型检查
│
├── 3. 易于维护
│ ├── 校验规则集中管理
│ ├── 修改规则只需修改 DTO
│ └── 不影响其他代码
│
├── 4. 可复用
│ ├── DTO 可在多个 Controller 中使用
│ ├── 支持继承和组合
│ └── 减少重复代码
│
└── 5. 文档化
├── DTO 定义即文档
├── 清晰的数据结构
└── 易于团队协作八、完整代码示例
8.1 项目结构
src/
├── prisma/
│ ├── prisma.module.ts
│ └── prisma.service.ts
├── modules/
│ └── home/
│ ├── home.module.ts
│ ├── home.controller.ts
│ ├── home.service.ts
│ └── dto/
│ └── create-home-resource.dto.ts
├── app.module.ts
└── main.ts8.2 完整代码清单
1. main.ts(全局配置):
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 配置全局 ValidationPipe
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: false,
disableErrorMessages: false,
}),
);
// 设置默认版本
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
await app.listen(3000);
}
bootstrap();2. create-home-resource.dto.ts(DTO 定义):
// src/modules/home/dto/create-home-resource.dto.ts
import {
IsString,
IsOptional,
IsUrl,
IsInt,
IsIn,
Min,
} from 'class-validator';
export class CreateHomeResourceDto {
@IsOptional()
@IsString({ message: 'title 必须是字符串' })
title?: string;
@IsOptional()
@IsString({ message: 'subtitle 必须是字符串' })
subtitle?: string;
@IsOptional()
@IsUrl({}, { message: 'url 格式不正确' })
url?: string;
@IsOptional()
@IsString({ message: 'image 必须是字符串' })
image?: string;
@IsOptional()
@IsString({ message: 'desc 必须是字符串' })
desc?: string;
@IsIn(['home', 'study'], { message: 'module 必须是 home 或 study' })
module: string;
@IsOptional()
@IsString({ message: 'type 必须是字符串' })
type?: string;
@IsOptional()
@IsString({ message: 'icon 必须是字符串' })
icon?: string;
@IsOptional()
@IsInt({ message: 'order 必须是整数' })
@Min(0, { message: 'order 不能小于 0' })
order?: number;
}3. home.controller.ts(Controller 层):
// src/modules/home/home.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { HomeService } from './home.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
@Controller('home')
export class HomeController {
constructor(private readonly homeService: HomeService) {}
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
return this.homeService.create(createDto);
}
}4. home.service.ts(Service 层):
// src/modules/home/home.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async create(createDto: CreateHomeResourceDto) {
return this.prisma.homeResources.create({
data: createDto,
});
}
}8.3 测试脚本
使用 Postman 测试:
# 测试 1:校验成功
POST http://localhost:3000/v1/home
Content-Type: application/json
{
"title": "新资源",
"url": "https://example.com",
"module": "home",
"order": 100
}
# 响应
{
"id": 1,
"title": "新资源",
"url": "https://example.com",
"module": "home",
"order": 100,
...
}# 测试 2:类型错误
POST http://localhost:3000/v1/home
Content-Type: application/json
{
"title": 123,
"module": "home"
}
# 响应
{
"statusCode": 400,
"message": ["title 必须是字符串"],
"error": "Bad Request"
}# 测试 3:格式错误
POST http://localhost:3000/v1/home
Content-Type: application/json
{
"url": "invalid-url",
"module": "home"
}
# 响应
{
"statusCode": 400,
"message": ["url 格式不正确"],
"error": "Bad Request"
}九、常见问题与解决方案
9.1 校验不生效
问题:配置了 DTO,但校验不生效。
原因:
- 没有安装 class-validator 和 class-transformer
- 没有配置全局 ValidationPipe
- 没有在 Controller 方法中使用 DTO 类型
解决方案:
// 1. 安装依赖
$ pnpm add class-validator class-transformer
// 2. 配置全局 ValidationPipe
app.useGlobalPipes(new ValidationPipe());
// 3. 在 Controller 中使用 DTO 类型
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
return this.homeService.create(createDto);
}9.2 默认版本未生效
问题:设置了 defaultVersion: '1',但访问 /home 提示 404。
原因:没有正确配置版本控制。
解决方案:
// main.ts
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1', // 设置默认版本
});
// 现在可以访问
// GET http://localhost:3000/home(自动使用 v1)
// 等价于
// GET http://localhost:3000/v1/home9.3 DTO 字段定义错误
问题:DTO 定义时使用了 type 关键字。
原因:混淆了 TypeScript 的 type 和 class。
错误示例:
// 错误:使用 type
export type CreateHomeResourceDto = {
title?: string;
};正确示例:
// 正确:使用 class
export class CreateHomeResourceDto {
@IsOptional()
@IsString()
title?: string;
}说明:
- DTO 必须使用
class,不能使用type或interface class可以添加装饰器,type不能- ValidationPipe 只支持
class
十、学习要点总结
10.1 核心知识点
本节核心知识点:
│
├── 新增功能完整流程
│ ├── 前端请求 → ValidationPipe → Controller → Service → Prisma → Database
│ └── 校验失败不进入 Controller,提升性能
│
├── ValidationPipe 配置
│ ├── 全局配置:app.useGlobalPipes(new ValidationPipe())
│ ├── transform: true(自动类型转换)
│ ├── whitelist: true(白名单机制)
│ └── forbidNonWhitelisted: true(拒绝恶意字段)
│
├── DTO 创建
│ ├── 文件命名:create-home-resource.dto.ts
│ ├── 使用 class 定义,不能使用 type
│ ├── 对照 Schema 创建字段
│ └── 使用装饰器定义校验规则
│
├── 常用装饰器
│ ├── @IsOptional():可选字段
│ ├── @IsString():字符串类型
│ ├── @IsInt():整数类型
│ ├── @IsUrl():URL 格式
│ ├── @IsIn([]):枚举值
│ └── @Min():最小值
│
├── Controller 实现
│ ├── @Post():POST 请求
│ ├── @Body():获取请求体
│ └── 参数类型:CreateHomeResourceDto
│
├── Service 实现
│ ├── 接收 DTO 实例
│ └── 调用 Prisma create() 方法
│
└── 测试验证
├── 类型错误测试
├── 格式错误测试
├── 枚举值错误测试
└── 校验成功测试10.2 学习路径规划
学习路径:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解新增功能的完整流程
│ ├── 理解 DTO 的作用和优势
│ └── 理解 ValidationPipe 的工作原理
│
├── 第二阶段:实践使用(2-3 天)
│ ├── 创建 DTO 定义
│ ├── 实现 Controller 和 Service
│ ├── 测试校验功能
│ └── 处理各种错误场景
│
└── 第三阶段:深入应用(持续)
├── 复杂的校验规则
├── 自定义校验装饰器
├── 嵌套对象校验
└── 分组校验10.3 重要程度标注
重要程度说明:
│
├── 必须掌握
│ ├── 新增功能的完整流程
│ ├── ValidationPipe 全局配置
│ ├── 创建 DTO 并使用装饰器
│ ├── Controller 中使用 @Body() 和 DTO
│ └── Service 中使用 Prisma create()
│
├── 重要
│ ├── DTO 文件命名规范
│ ├── 常用装饰器的使用
│ ├── DTO 与 Schema 的对应关系
│ └── 测试校验功能
│
└── 了解
├── 自定义错误消息
├── DTO 继承和复用
└── 高级校验规则重要提示:这一节是 NestJS 新增功能实现的核心内容,理解 DTO 的创建和使用、熟练使用 ValidationPipe 进行数据校验,对实际项目开发非常重要!特别是要掌握如何对照 Schema 创建 DTO,以及如何使用装饰器定义校验规则!
下一节预告:下一节将学习更新功能和删除功能的实现,包括 Update DTO 的创建、PartialType 的使用、Prisma update 和 delete 方法的使用。