{T}

NestJS完整CRUD操作实现

NestJS完整CRUD操作实现

学习目标:掌握 Service 层 CRUD 方法实现、Update DTO 的创建和继承、Delete 操作实现、DefaultValuePipe 使用、Controller 路由匹配规则、异常处理最佳实践。


一、CRUD 完整实现流程

1.1 CRUD 操作概览

code
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 层方法定义

typescript
// 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 的创建方式

code
Update DTO 创建方式对比:
│
├── 方式一:手动定义所有字段(繁琐)
│   ├── 优点:完全控制字段
│   ├── 缺点:重复代码多
│   └── 不推荐:维护成本高
│
├── 方式二:继承 Create DTO(简单)
│   ├── 优点:复用字段定义
│   ├── 缺点:无法排除某些字段
│   └── 适用:字段完全相同
│
├── 方式三:使用 PartialType(推荐)
│   ├── 优点:所有字段变为可选
│   ├── 缺点:无法排除某些字段
│   └── 适用:更新时字段可选
│
└── 方式四:使用 OmitType(最佳)
    ├── 优点:排除不需要的字段
    ├── 优点:结合 PartialType
    ├── 优点:灵活控制字段
    └── 推荐:生产环境使用

2.2 方式一:手动定义所有字段

typescript
// 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

typescript
// 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

typescript
// 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()

示例对比

typescript
// 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(推荐)

安装依赖

bash
# NestJS 提供的工具库
$ pnpm add @nestjs/mapped-types

使用 OmitType

typescript
// 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 使所有字段可选

工作原理

code
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
//  TypeScript Omit:只能用于 type,不能用于 class
type UpdateDto = Omit<CreateHomeResourceDto, 'id'>;

//  NestJS OmitType:可以用于 class,保留装饰器
export class UpdateHomeResourceDto extends OmitType(
  CreateHomeResourceDto,
  ['id'] as const,
) {}

对比总结

特性TypeScript OmitNestJS OmitType
适用类型typeclass
装饰器不保留保留
ValidationPipe不支持支持
运行时不存在存在

2.7 @nestjs/mapped-types 工具函数

typescript
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 层

typescript
// 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 层

typescript
// 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 方法详解

typescript
// 删除单条记录
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() 装饰器

typescript
// 方式一:获取所有路径参数
@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 设置参数默认值

问题场景

  • 分页查询时,前端可能不传递 pagesize 参数
  • 需要设置默认值,避免校验失败

使用 DefaultValuePipe

typescript
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 关键字
  • 顺序:DefaultValuePipeParseIntPipe
  • 如果前端不传参数,使用默认值

请求示例

bash
# 不传参数,使用默认值
GET /home
# page = 1, size = 10

# 传递参数,使用传递的值
GET /home?page=2&size=20
# page = 2, size = 20

4.2 Pipe 执行顺序

code
Pipe 执行顺序(从左到右):
│
├── 第一步:DefaultValuePipe(1)
│   └── 如果参数不存在,设置默认值 1
│
├── 第二步:ParseIntPipe
│   └── 将 string 转换为 number
│
└── 第三步:赋值给参数
    └── page: number

错误示例

typescript
//  错误:顺序颠倒
@Query('page', ParseIntPipe, new DefaultValuePipe(1)) page: number
// 结果:如果参数不存在,ParseIntPipe 会报错

//  正确:DefaultValuePipe 在前
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number
// 结果:如果参数不存在,先设置默认值,再转换类型

五、Controller 路由匹配规则

5.1 路由匹配规则

code
NestJS Controller 路由匹配规则:
│
├── 规则一:从上到下匹配
│   ├── 按照方法定义的顺序
│   └── 第一个匹配的路由生效
│
├── 规则二:静态路径优先
│   ├── 固定路径优先匹配
│   └── 动态参数路径后匹配
│
└── 规则三:动态参数匹配任意值
    ├── :id 匹配除 / 外的任意字符串
    └── 需要放在最后定义

5.2 正确的路由定义顺序

** 错误示例:动态路由在前**:

typescript
@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 方法

** 正确示例:静态路由在前**:

typescript
@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 路由匹配示例

code
路由匹配示例:
│
├── 定义顺序:
│   ├── @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 最佳实践总结

code
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

异常处理示例

typescript
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 使用

基本用法

typescript
import { NotFoundException } from '@nestjs/common';

// 方式一:简单消息
throw new NotFoundException('资源不存在');

// 方式二:详细错误
throw new NotFoundException({
  statusCode: 404,
  message: '资源不存在',
  error: 'Not Found',
});

// 响应格式
{
  "statusCode": 404,
  "message": "资源不存在",
  "error": "Not Found"
}

在 Service 中使用

typescript
async findOne(id: number) {
  const resource = await this.prisma.homeResources.findUnique({
    where: { id },
  });

  if (!resource) {
    throw new NotFoundException(`ID 为 ${id} 的资源不存在`);
  }

  return resource;
}

6.3 完整的异常处理示例

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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 完整测试流程

测试脚本

bash
# 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 提供的工具函数。

typescript
//  错误:直接继承,装饰器不生效
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 顺序错误。

解决方案

typescript
//  错误:ParseIntPipe 在前
@Query('page', ParseIntPipe, new DefaultValuePipe(1)) page: number

//  正确:DefaultValuePipe 在前
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number

8.3 Delete 路由匹配错误

问题DELETE /home/batch 被匹配到 @Delete(':id')

原因:动态路由在静态路由之前定义。

解决方案:将静态路由放在动态路由之前。

typescript
//  正确顺序
@Controller('home')
export class HomeController {
  @Delete('batch')  // 静态路由在前
  async batchRemove() {}

  @Delete(':id')    // 动态路由在后
  async remove() {}
}

九、学习要点总结

9.1 核心知识点

code
本节核心知识点:
│
├── 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 学习路径规划

code
学习路径:
│
├── 第一阶段:理解概念(1 天)
│   ├── 理解 CRUD 完整流程
│   ├── 理解 DTO 的继承和复用
│   └── 理解路由匹配规则
│
├── 第二阶段:实践使用(2-3 天)
│   ├── 实现完整的 CRUD
│   ├── 使用 PartialType 和 OmitType
│   ├── 使用 DefaultValuePipe
│   └── 处理各种异常场景
│
└── 第三阶段:深入应用(持续)
    ├── 复杂业务逻辑
    ├── 批量操作
    └── 事务处理

9.3 重要程度标注

code
重要程度说明:
│
├──  必须掌握
│   ├── Service 层 CRUD 方法实现
│   ├── PartialType 和 OmitType 使用
│   ├── DefaultValuePipe 使用
│   ├── Controller 路由匹配规则
│   └── 异常处理最佳实践
│
├──  重要
│   ├── Prisma 错误码处理
│   ├── TypeScript Omit vs OmitType
│   ├── NotFoundException 使用
│   └── Update DTO 创建方式对比
│
└──  了解
    ├── PickType 和 IntersectionType
    ├── 批量删除操作
    └── 复杂异常处理

重要提示:这一节是 NestJS CRUD 完整实现的核心内容,掌握 Service 层方法实现、DTO 继承和复用、路由匹配规则、异常处理,对实际项目开发非常重要!特别是要理解 PartialTypeOmitType 的使用,以及路由匹配的顺序规则!

下一节预告:下一节将学习关联查询和表关系处理,包括一对一、一对多、多对多关系的实现。