NestJS完整CRUD操作实现
NestJS完整CRUD操作实现
学习目标:掌握 Service 层 CRUD 方法实现、Update DTO 的创建和继承、Delete 操作实现、DefaultValuePipe 使用、Controller 路由匹配规则、异常处理最佳实践。
一、CRUD 完整实现流程
1.1 CRUD 操作概览
CRUD 操作对应表:
│
├── Create(创建)
│ ├── HTTP 方法:POST
│ ├── 路径:/home
│ ├── 参数:@Body() createDto
│ ├── Service:create(createDto)
│ └── Prisma:prisma.model.create({ data })
│
├── Read(查询)
│ ├── HTTP 方法:GET
│ ├── 路径:/home 或 /home/:id
│ ├── 参数:@Query() 或 @Param()
│ ├── Service:findAll() 或 findOne(id)
│ └── Prisma:prisma.model.findMany() 或 findUnique()
│
├── Update(更新)
│ ├── HTTP 方法:PUT 或 PATCH
│ ├── 路径:/home
│ ├── 参数:@Body() updateDto
│ ├── Service:update(updateDto)
│ └── Prisma:prisma.model.update({ where, data })
│
└── Delete(删除)
├── HTTP 方法:DELETE
├── 路径:/home/:id
├── 参数:@Param('id') id
├── Service:remove(id)
└── Prisma:prisma.model.delete({ where })1.2 Service 层方法定义
// src/modules/home/home.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
import { UpdateHomeResourceDto } from './dto/update-home-resource.dto';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
// 查询列表
async findAll(page: number = 1, size: number = 10) {
const skip = (page - 1) * size;
const take = size;
const [data, total] = await this.prisma.$transaction([
this.prisma.homeResources.findMany({
skip,
take,
orderBy: { order: 'asc' },
}),
this.prisma.homeResources.count(),
]);
return [data, total];
}
// 创建
async create(createDto: CreateHomeResourceDto) {
return this.prisma.homeResources.create({
data: createDto,
});
}
// 更新
async update(updateDto: UpdateHomeResourceDto) {
const { id, ...data } = updateDto;
return this.prisma.homeResources.update({
where: { id },
data,
});
}
// 删除
async remove(id: number) {
try {
return await this.prisma.homeResources.delete({
where: { id },
});
} catch (error) {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
}
}二、Update DTO 的创建与继承
2.1 Update DTO 的创建方式
Update DTO 创建方式对比:
│
├── 方式一:手动定义所有字段(繁琐)
│ ├── 优点:完全控制字段
│ ├── 缺点:重复代码多
│ └── 不推荐:维护成本高
│
├── 方式二:继承 Create DTO(简单)
│ ├── 优点:复用字段定义
│ ├── 缺点:无法排除某些字段
│ └── 适用:字段完全相同
│
├── 方式三:使用 PartialType(推荐)
│ ├── 优点:所有字段变为可选
│ ├── 缺点:无法排除某些字段
│ └── 适用:更新时字段可选
│
└── 方式四:使用 OmitType(最佳)
├── 优点:排除不需要的字段
├── 优点:结合 PartialType
├── 优点:灵活控制字段
└── 推荐:生产环境使用2.2 方式一:手动定义所有字段
// src/modules/home/dto/update-home-resource.dto.ts
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUrl,
IsIn,
Min,
} from 'class-validator';
export class UpdateHomeResourceDto {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
@IsOptional()
@IsString({ message: 'title 必须是字符串' })
title?: string;
@IsOptional()
@IsString({ message: 'subtitle 必须是字符串' })
subtitle?: string;
@IsOptional()
@IsUrl({}, { message: 'url 格式不正确' })
url?: string;
// ... 其他字段
}缺点:
- 需要手动复制所有字段
- 字段多时代码冗长
- 维护成本高
2.3 方式二:继承 Create DTO
// src/modules/home/dto/update-home-resource.dto.ts
import { IsNotEmpty, IsNumber } from 'class-validator';
import { CreateHomeResourceDto } from './create-home-resource.dto';
export class UpdateHomeResourceDto extends CreateHomeResourceDto {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
}问题:
- Create DTO 中的必填字段在 Update 时也是必填
- 无法控制哪些字段可以更新
- 不够灵活
2.4 方式三:使用 PartialType
// src/modules/home/dto/update-home-resource.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsNumber } from 'class-validator';
import { CreateHomeResourceDto } from './create-home-resource.dto';
export class UpdateHomeResourceDto extends PartialType(CreateHomeResourceDto) {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
}PartialType 的作用:
- 将所有字段变为可选(Optional)
- 等价于在每个字段上添加
@IsOptional()
示例对比:
// CreateHomeResourceDto
export class CreateHomeResourceDto {
@IsString()
title: string; // 必填
@IsOptional()
subtitle?: string; // 可选
}
// UpdateHomeResourceDto(使用 PartialType)
export class UpdateHomeResourceDto extends PartialType(CreateHomeResourceDto) {
@IsNotEmpty()
id: number; // 必填
// title 自动变为可选
// subtitle 依然可选
}2.5 方式四:使用 OmitType(推荐)
安装依赖:
# NestJS 提供的工具库
$ pnpm add @nestjs/mapped-types使用 OmitType:
// src/modules/home/dto/update-home-resource.dto.ts
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsNumber } from 'class-validator';
import { CreateHomeResourceDto } from './create-home-resource.dto';
// 方式一:OmitType 排除 id,然后添加 id 并设为必填
export class UpdateHomeResourceDto extends OmitType(
CreateHomeResourceDto,
['id'] as const, // 排除 id 字段(因为 Create DTO 中没有 id)
) {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
}
// 方式二:结合 PartialType 和 OmitType(最常用)
export class UpdateHomeResourceDto extends PartialType(
OmitType(CreateHomeResourceDto, ['id'] as const),
) {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
}OmitType 的作用:
- 从基类中排除指定的字段
- 结合 PartialType 使所有字段可选
工作原理:
UpdateHomeResourceDto 创建过程:
│
├── 第一步:CreateHomeResourceDto
│ └── { title, subtitle, url, ... } // 所有字段
│
├── 第二步:OmitType(CreateHomeResourceDto, ['id'])
│ └── { title, subtitle, url, ... } // 排除 id(如果存在)
│
├── 第三步:PartialType(...)
│ └── { title?, subtitle?, url?, ... } // 所有字段变为可选
│
└── 第四步:添加 id 字段
└── { id, title?, subtitle?, url?, ... } // id 必填,其他可选2.6 TypeScript Omit vs NestJS OmitType
重要区别:
// TypeScript Omit:只能用于 type,不能用于 class
type UpdateDto = Omit<CreateHomeResourceDto, 'id'>;
// NestJS OmitType:可以用于 class,保留装饰器
export class UpdateHomeResourceDto extends OmitType(
CreateHomeResourceDto,
['id'] as const,
) {}对比总结:
| 特性 | TypeScript Omit | NestJS OmitType |
|---|---|---|
| 适用类型 | type | class |
| 装饰器 | 不保留 | 保留 |
| ValidationPipe | 不支持 | 支持 |
| 运行时 | 不存在 | 存在 |
2.7 @nestjs/mapped-types 工具函数
import {
PartialType, // 所有字段变为可选
OmitType, // 排除指定字段
PickType, // 选择指定字段
IntersectionType, // 合并多个 DTO
} from '@nestjs/mapped-types';
// PartialType:所有字段可选
export class UpdateDto extends PartialType(CreateDto) {}
// OmitType:排除字段
export class UpdateDto extends OmitType(CreateDto, ['id'] as const) {}
// PickType:选择字段
export class PublicUserDto extends PickType(UserDto, ['id', 'name'] as const) {}
// IntersectionType:合并 DTO
export class UserWithProfileDto extends IntersectionType(
UserDto,
ProfileDto,
) {}三、Delete 操作实现
3.1 Delete 操作完整实现
Controller 层:
// src/modules/home/home.controller.ts
import { Controller, Delete, Param, ParseIntPipe } from '@nestjs/common';
import { HomeService } from './home.service';
@Controller('home')
export class HomeController {
constructor(private readonly homeService: HomeService) {}
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number) {
return this.homeService.remove(id);
}
}Service 层:
// src/modules/home/home.service.ts
async remove(id: number) {
try {
return await this.prisma.homeResources.delete({
where: { id },
});
} catch (error) {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
}关键点说明:
@Delete(':id'):定义 DELETE 请求和路径参数@Param('id', ParseIntPipe):获取路径参数并转换为数字try-catch:捕获删除失败的异常NotFoundException:返回 404 错误
3.2 Prisma delete 方法详解
// 删除单条记录
const deleted = await this.prisma.homeResources.delete({
where: { id: 1 },
});
// 返回值:被删除的记录对象
{
"id": 1,
"title": "被删除的资源",
...
}
// 如果记录不存在,抛出异常
// PrismaClientKnownRequestError: Code: P2025
// Record to delete does not exist错误码说明:
| 错误码 | 说明 | 处理方式 |
|---|---|---|
P2025 | 记录不存在 | 返回 404 |
P2003 | 外键约束失败 | 返回 400 |
P2014 | 关联记录不存在 | 返回 400 |
3.3 路径参数获取
使用 @Param() 装饰器:
// 方式一:获取所有路径参数
@Delete(':id')
async remove(@Param() params: any) {
const id = params.id;
return this.homeService.remove(id);
}
// 方式二:获取指定路径参数(推荐)
@Delete(':id')
async remove(@Param('id') id: string) {
return this.homeService.remove(+id); // 转换为数字
}
// 方式三:使用 ParseIntPipe 自动转换(最佳)
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number) {
return this.homeService.remove(id); // 已经是数字
}参数转换对比:
| 方式 | 类型 | 说明 |
|---|---|---|
@Param('id') | string | 需要手动转换 |
@Param('id', ParseIntPipe) | number | 自动转换 |
四、DefaultValuePipe 使用
4.1 设置参数默认值
问题场景:
- 分页查询时,前端可能不传递
page和size参数 - 需要设置默认值,避免校验失败
使用 DefaultValuePipe:
import { Controller, Get, Query, DefaultValuePipe, ParseIntPipe } from '@nestjs/common';
@Controller('home')
export class HomeController {
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,
) {
return this.homeService.findAll(page, size);
}
}关键点:
DefaultValuePipe需要使用new关键字- 顺序:
DefaultValuePipe→ParseIntPipe - 如果前端不传参数,使用默认值
请求示例:
# 不传参数,使用默认值
GET /home
# page = 1, size = 10
# 传递参数,使用传递的值
GET /home?page=2&size=20
# page = 2, size = 204.2 Pipe 执行顺序
Pipe 执行顺序(从左到右):
│
├── 第一步:DefaultValuePipe(1)
│ └── 如果参数不存在,设置默认值 1
│
├── 第二步:ParseIntPipe
│ └── 将 string 转换为 number
│
└── 第三步:赋值给参数
└── page: number错误示例:
// 错误:顺序颠倒
@Query('page', ParseIntPipe, new DefaultValuePipe(1)) page: number
// 结果:如果参数不存在,ParseIntPipe 会报错
// 正确:DefaultValuePipe 在前
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number
// 结果:如果参数不存在,先设置默认值,再转换类型五、Controller 路由匹配规则
5.1 路由匹配规则
NestJS Controller 路由匹配规则:
│
├── 规则一:从上到下匹配
│ ├── 按照方法定义的顺序
│ └── 第一个匹配的路由生效
│
├── 规则二:静态路径优先
│ ├── 固定路径优先匹配
│ └── 动态参数路径后匹配
│
└── 规则三:动态参数匹配任意值
├── :id 匹配除 / 外的任意字符串
└── 需要放在最后定义5.2 正确的路由定义顺序
** 错误示例:动态路由在前**:
@Controller('home')
export class HomeController {
@Delete(':id')
async remove(@Param('id') id: string) {
return this.homeService.remove(+id);
}
@Delete('batch')
async batchRemove(@Body() ids: number[]) {
return this.homeService.batchRemove(ids);
}
}
// 问题:
// DELETE /home/batch 会匹配到 :id 路由
// id 的值会是 'batch',而不是调用 batchRemove 方法** 正确示例:静态路由在前**:
@Controller('home')
export class HomeController {
// 静态路由:批量删除(放在前面)
@Delete('batch')
async batchRemove(@Body() ids: number[]) {
return this.homeService.batchRemove(ids);
}
// 动态路由:删除单个(放在后面)
@Delete(':id')
async remove(@Param('id') id: string) {
return this.homeService.remove(+id);
}
}
// 结果:
// DELETE /home/batch → 匹配 batchRemove 方法
// DELETE /home/1 → 匹配 remove 方法 5.3 路由匹配示例
路由匹配示例:
│
├── 定义顺序:
│ ├── @Delete('batch') # 静态路由
│ └── @Delete(':id') # 动态路由
│
├── 请求:DELETE /home/batch
│ ├── 匹配:@Delete('batch')
│ └── 结果:调用 batchRemove()
│
├── 请求:DELETE /home/1
│ ├── 匹配:@Delete(':id')
│ └── 结果:调用 remove(1)
│
└── 请求:DELETE /home/abc
├── 匹配:@Delete(':id')
└── 结果:调用 remove('abc')5.4 最佳实践总结
Controller 路由定义最佳实践:
│
├── 1. 静态路由在前
│ ├── @Get('list')
│ ├── @Post('batch')
│ └── @Delete('clear')
│
├── 2. 动态路由在后
│ ├── @Get(':id')
│ ├── @Put(':id')
│ └── @Delete(':id')
│
├── 3. 避免歧义路由
│ ├── @Get(':id') 和 @Get('list') 同时存在
│ └── @Get('list') 和 @Get(':id') 按顺序定义
│
└── 4. 使用有意义的参数名
├── @Get(':id')
├── @Get(':slug')
└── @Get(':x')六、异常处理最佳实践
6.1 Prisma 异常处理
常见的 Prisma 错误码:
| 错误码 | 说明 | HTTP 状态码 |
|---|---|---|
| P2025 | 记录不存在 | 404 |
| P2002 | 唯一约束冲突 | 409 |
| P2003 | 外键约束失败 | 400 |
| P2014 | 关联记录不存在 | 400 |
| P2021 | 表不存在 | 500 |
异常处理示例:
import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async remove(id: number) {
try {
return await this.prisma.homeResources.delete({
where: { id },
});
} catch (error) {
// 判断 Prisma 错误类型
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
}
// 其他错误
throw error;
}
}
async create(createDto: CreateHomeResourceDto) {
try {
return await this.prisma.homeResources.create({
data: createDto,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ConflictException('资源已存在');
}
}
throw error;
}
}
}6.2 NotFoundException 使用
基本用法:
import { NotFoundException } from '@nestjs/common';
// 方式一:简单消息
throw new NotFoundException('资源不存在');
// 方式二:详细错误
throw new NotFoundException({
statusCode: 404,
message: '资源不存在',
error: 'Not Found',
});
// 响应格式
{
"statusCode": 404,
"message": "资源不存在",
"error": "Not Found"
}在 Service 中使用:
async findOne(id: number) {
const resource = await this.prisma.homeResources.findUnique({
where: { id },
});
if (!resource) {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
return resource;
}6.3 完整的异常处理示例
// src/modules/home/home.service.ts
import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async remove(id: number) {
try {
return await this.prisma.homeResources.delete({
where: { id },
});
} catch (error) {
// Prisma 已知错误
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2025':
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
case 'P2003':
throw new BadRequestException('存在关联数据,无法删除');
default:
throw error;
}
}
// 未知错误
throw error;
}
}
}七、完整 CRUD 实战示例
7.1 完整代码清单
1. create-home-resource.dto.ts:
// 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;
}2. update-home-resource.dto.ts:
// src/modules/home/dto/update-home-resource.dto.ts
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsNumber } from 'class-validator';
import { CreateHomeResourceDto } from './create-home-resource.dto';
export class UpdateHomeResourceDto extends PartialType(
OmitType(CreateHomeResourceDto, [] as const),
) {
@IsNotEmpty({ message: 'id 不能为空' })
@IsNumber({}, { message: 'id 必须是数字' })
id: number;
}3. home.service.ts:
// src/modules/home/home.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
import { UpdateHomeResourceDto } from './dto/update-home-resource.dto';
@Injectable()
export class HomeService {
constructor(private prisma: PrismaService) {}
async findAll(page: number = 1, size: number = 10) {
const skip = (page - 1) * size;
const take = size;
const [data, total] = await this.prisma.$transaction([
this.prisma.homeResources.findMany({
skip,
take,
orderBy: { order: 'asc' },
}),
this.prisma.homeResources.count(),
]);
return [data, total];
}
async create(createDto: CreateHomeResourceDto) {
return this.prisma.homeResources.create({
data: createDto,
});
}
async update(updateDto: UpdateHomeResourceDto) {
const { id, ...data } = updateDto;
try {
return await this.prisma.homeResources.update({
where: { id },
data,
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
throw error;
}
}
async remove(id: number) {
try {
return await this.prisma.homeResources.delete({
where: { id },
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
}
throw error;
}
}
}4. home.controller.ts:
// src/modules/home/home.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
ParseIntPipe,
DefaultValuePipe,
} from '@nestjs/common';
import { HomeService } from './home.service';
import { CreateHomeResourceDto } from './dto/create-home-resource.dto';
import { UpdateHomeResourceDto } from './dto/update-home-resource.dto';
@Controller('home')
export class HomeController {
constructor(private readonly homeService: HomeService) {}
// 查询列表
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,
) {
const [data, total] = await this.homeService.findAll(page, size);
return { data, total };
}
// 创建
@Post()
async create(@Body() createDto: CreateHomeResourceDto) {
return this.homeService.create(createDto);
}
// 更新
@Put()
async update(@Body() updateDto: UpdateHomeResourceDto) {
return this.homeService.update(updateDto);
}
// 删除
@Delete(':id')
async remove(@Param('id', ParseIntPipe) id: number) {
return this.homeService.remove(id);
}
}7.2 完整测试流程
测试脚本:
# 1. 查询列表(GET)
GET http://localhost:3000/v1/home
GET http://localhost:3000/v1/home?page=1&size=10
# 响应
{
"data": [...],
"total": 100
}
# 2. 创建(POST)
POST http://localhost:3000/v1/home
Content-Type: application/json
{
"title": "新资源",
"url": "https://example.com",
"module": "home"
}
# 响应
{
"id": 1,
"title": "新资源",
...
}
# 3. 更新(PUT)
PUT http://localhost:3000/v1/home
Content-Type: application/json
{
"id": 1,
"title": "更新后的标题"
}
# 响应
{
"id": 1,
"title": "更新后的标题",
...
}
# 4. 删除(DELETE)
DELETE http://localhost:3000/v1/home/1
# 响应
{
"id": 1,
"title": "被删除的资源",
...
}
# 5. 删除不存在的记录
DELETE http://localhost:3000/v1/home/999
# 响应
{
"statusCode": 404,
"message": "ID 为 999 的资源不存在",
"error": "Not Found"
}八、常见问题与解决方案
8.1 Update DTO 字段校验不生效
问题:继承 Create DTO 后,字段校验不生效。
原因:class-validator 默认不会继承父类的装饰器。
解决方案:使用 @nestjs/mapped-types 提供的工具函数。
// 错误:直接继承,装饰器不生效
export class UpdateDto extends CreateDto {}
// 正确:使用 PartialType
export class UpdateDto extends PartialType(CreateDto) {}
// 正确:使用 OmitType
export class UpdateDto extends OmitType(CreateDto, ['id'] as const) {}8.2 DefaultValuePipe 不生效
问题:使用 DefaultValuePipe 后,参数仍然为 undefined。
原因:Pipe 顺序错误。
解决方案:
// 错误:ParseIntPipe 在前
@Query('page', ParseIntPipe, new DefaultValuePipe(1)) page: number
// 正确:DefaultValuePipe 在前
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number8.3 Delete 路由匹配错误
问题:DELETE /home/batch 被匹配到 @Delete(':id')。
原因:动态路由在静态路由之前定义。
解决方案:将静态路由放在动态路由之前。
// 正确顺序
@Controller('home')
export class HomeController {
@Delete('batch') // 静态路由在前
async batchRemove() {}
@Delete(':id') // 动态路由在后
async remove() {}
}九、学习要点总结
9.1 核心知识点
本节核心知识点:
│
├── Service 层 CRUD 方法
│ ├── create(createDto):创建
│ ├── findAll(page, size):查询列表
│ ├── update(updateDto):更新
│ └── remove(id):删除
│
├── Update DTO 创建方式
│ ├── 方式一:手动定义(繁琐)
│ ├── 方式二:继承 Create DTO(简单)
│ ├── 方式三:PartialType(推荐)
│ └── 方式四:OmitType + PartialType(最佳)
│
├── @nestjs/mapped-types 工具
│ ├── PartialType:所有字段可选
│ ├── OmitType:排除字段
│ ├── PickType:选择字段
│ └── IntersectionType:合并 DTO
│
├── DefaultValuePipe 使用
│ ├── 设置参数默认值
│ ├── 必须使用 new 关键字
│ └── 放在其他 Pipe 之前
│
├── Controller 路由匹配规则
│ ├── 从上到下匹配
│ ├── 静态路由优先
│ └── 动态路由在后
│
└── 异常处理
├── Prisma 错误码处理
├── NotFoundException:404
└── try-catch 捕获异常9.2 学习路径规划
学习路径:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解 CRUD 完整流程
│ ├── 理解 DTO 的继承和复用
│ └── 理解路由匹配规则
│
├── 第二阶段:实践使用(2-3 天)
│ ├── 实现完整的 CRUD
│ ├── 使用 PartialType 和 OmitType
│ ├── 使用 DefaultValuePipe
│ └── 处理各种异常场景
│
└── 第三阶段:深入应用(持续)
├── 复杂业务逻辑
├── 批量操作
└── 事务处理9.3 重要程度标注
重要程度说明:
│
├── 必须掌握
│ ├── Service 层 CRUD 方法实现
│ ├── PartialType 和 OmitType 使用
│ ├── DefaultValuePipe 使用
│ ├── Controller 路由匹配规则
│ └── 异常处理最佳实践
│
├── 重要
│ ├── Prisma 错误码处理
│ ├── TypeScript Omit vs OmitType
│ ├── NotFoundException 使用
│ └── Update DTO 创建方式对比
│
└── 了解
├── PickType 和 IntersectionType
├── 批量删除操作
└── 复杂异常处理重要提示:这一节是 NestJS CRUD 完整实现的核心内容,掌握 Service 层方法实现、DTO 继承和复用、路由匹配规则、异常处理,对实际项目开发非常重要!特别是要理解 PartialType 和 OmitType 的使用,以及路由匹配的顺序规则!
下一节预告:下一节将学习关联查询和表关系处理,包括一对一、一对多、多对多关系的实现。