{T}

NestJS业务接口开发与API版本控制

NestJS业务接口开发与API版本控制

学习目标:掌握 NestJS 模块创建、RESTful 接口规范、API 版本控制和数据库迁移流程。


一、数据库表结构修改与迁移

1.1 修改表字段

修改 prisma/schema.prisma

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")
}

字段说明

  • order:排序字段,默认值为 100
  • 用于控制首页资源的显示顺序

1.2 创建数据库迁移

迁移工作流程

bash
# 第一步:修改 schema.prisma 文件

# 第二步:创建迁移
$ pnpm migrate:create

# 或使用完整命令
$ npx prisma migrate dev --name add_order_field

# 第三步:查看生成的迁移文件
# prisma/migrations/20240101000000_add_order_field/migration.sql

生成的迁移文件

sql
-- AlterTable
ALTER TABLE "home_resources" ADD COLUMN "order" INTEGER NOT NULL DEFAULT 100;

迁移命令对比

命令作用使用场景
prisma migrate dev创建迁移 + 应用开发环境
prisma migrate deploy只应用迁移生产环境
prisma db push直接推送(不创建迁移)快速原型

1.3 验证数据库变化

方式一:使用 Prisma Studio

bash
$ npx prisma studio

方式二:使用 Adminer

访问 http://localhost:8080,查看表结构变化。

方式三:使用 Navicat 等工具

连接数据库,查看 home_resources 表结构。


二、创建业务模块

2.1 模块创建流程

使用 NestJS CLI 创建模块

bash
# 创建模块
$ nest g module modules/home

# 创建服务(不创建测试文件)
$ nest g service modules/home --no-spec

# 创建控制器(不创建测试文件)
$ nest g controller modules/home --no-spec

生成的文件结构

code
src/
├── modules/
│   └── home/
│       ├── home.module.ts      # 模块定义
│       ├── home.service.ts     # 服务层
│       └── home.controller.ts  # 控制器层
└── app.module.ts               # 根模块

2.2 文件内容详解

src/modules/home/home.module.ts

typescript
import { Module } from '@nestjs/common';
import { HomeController } from './home.controller';
import { HomeService } from './home.service';

@Module({
  controllers: [HomeController],  // 注册控制器
  providers: [HomeService],       // 注册服务
})
export class HomeModule {}

src/modules/home/home.service.ts

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

@Injectable()
export class HomeService {
  // 业务逻辑实现
}

src/modules/home/home.controller.ts

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

@Controller('home')
export class HomeController {
  // 控制器方法
}

src/app.module.ts(自动更新)

typescript
import { Module } from '@nestjs/common';
import { HomeModule } from './modules/home/home.module';

@Module({
  imports: [HomeModule],  // 自动导入模块
})
export class AppModule {}

2.3 NestJS CLI 命令详解

命令缩写作用生成文件
nest g modulenest g mo创建模块*.module.ts
nest g servicenest g s创建服务*.service.ts
nest g controllernest g co创建控制器*.controller.ts
nest g resourcenest g res创建完整资源模块+服务+控制器+DTO
--no-spec不创建测试文件跳过 *.spec.ts

三、RESTful 接口规范

3.1 RESTful 设计原则

code
RESTful API 设计原则:
│
├── 资源(Resources)
│   ├── 使用名词表示资源
│   ├── 使用复数形式
│   └── 示例:/users、/posts、/home-resources
│
├── HTTP 方法
│   ├── GET    → 获取资源
│   ├── POST   → 创建资源
│   ├── PUT    → 更新资源(整体)
│   ├── PATCH  → 更新资源(部分)
│   └── DELETE → 删除资源
│
├── 状态码
│   ├── 200 OK          → 成功
│   ├── 201 Created     → 创建成功
│   ├── 400 Bad Request → 请求错误
│   ├── 404 Not Found   → 资源不存在
│   └── 500 Server Error → 服务器错误
│
└── 路径设计
    ├── 获取列表:GET /users
    ├── 获取单个:GET /users/:id
    ├── 创建:POST /users
    ├── 更新:PUT /users/:id
    └── 删除:DELETE /users/:id

3.2 实现基础 CRUD 接口

src/modules/home/home.controller.ts

typescript
import { Controller, Get, Post, Put, Delete } from '@nestjs/common';

@Controller('home')
export class HomeController {
  // GET /home
  @Get()
  findAll() {
    return 'find';
  }

  // POST /home
  @Post()
  create() {
    return 'create';
  }

  // PUT /home
  @Put()
  update() {
    return 'update';
  }

  // DELETE /home
  @Delete()
  remove() {
    return 'delete';
  }
}

请求路径对应关系

HTTP 方法装饰器路径作用
GET@Get()/home查询所有
POST@Post()/home创建资源
PUT@Put()/home更新资源
DELETE@Delete()/home删除资源

3.3 自定义路径

添加子路径

typescript
@Controller('home')
export class HomeController {
  // GET /home/resources
  @Get('resources')
  findResources() {
    return 'find resources';
  }

  // GET /home/resources/:id
  @Get('resources/:id')
  findOne(@Param('id') id: string) {
    return `find resource ${id}`;
  }

  // POST /home/resources
  @Post('resources')
  create(@Body() data: any) {
    return { message: 'created', data };
  }
}

路径组成

code
完整路径 = 全局前缀 + Controller 前缀 + 方法路径

示例:
├── 全局前缀:api
├── Controller 前缀:home
├── 方法路径:resources
└── 完整路径:/api/home/resources

四、API 版本控制

4.1 为什么需要版本控制

code
API 版本控制的必要性:
│
├── 向后兼容
│   ├── 旧版本接口继续可用
│   ├── 不影响现有客户端
│   └── 平滑升级
│
├── 功能迭代
│   ├── 新功能在新版本发布
│   ├── 旧功能在旧版本保留
│   └── 灵活管理
│
└── 问题修复
    ├── 修复 bug 后发布新版本
    ├── 保留旧版本供兼容
    └── 逐步废弃

4.2 启用版本控制

src/main.ts

typescript
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 启用 URI 版本控制
  app.enableVersioning({
    type: VersioningType.URI,
  });

  await app.listen(3000);
}
bootstrap();

版本控制类型

类型说明示例
VersioningType.URIURI 路径版本/v1/users/v2/users
VersioningType.HEADER请求头版本Accept-Version: 1
VersioningType.MEDIA_TYPE媒体类型版本Accept: application/vnd.myapp.v1+json

4.3 使用 @Version 装饰器

方式一:控制器级别版本控制

typescript
import { Controller, Get, Version } from '@nestjs/common';

@Controller('home')
@Version('1')  // 整个控制器使用版本 1
export class HomeController {
  @Get()
  findAll() {
    return 'find v1';
  }
}

请求路径/v1/home

方式二:方法级别版本控制

typescript
import { Controller, Get, Version } from '@nestjs/common';

@Controller('home')
export class HomeController {
  @Get()
  @Version('1')
  findAllV1() {
    return 'find v1';
  }

  @Get()
  @Version('2')
  findAllV2() {
    return 'find v2';
  }
}

请求路径

  • /v1/home → 返回 'find v1'
  • /v2/home → 返回 'find v2'

4.4 设置默认版本

src/main.ts

typescript
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1',  // 默认版本为 1
  });

  await app.listen(3000);
}
bootstrap();

效果

  • 未设置 @Version() 的接口自动使用版本 1
  • 设置了 @Version() 的接口使用指定版本

4.5 支持多个版本

使用数组形式

typescript
app.enableVersioning({
  type: VersioningType.URI,
  defaultVersion: ['1', '2'],  // 同时支持版本 1 和 2
});

控制器示例

typescript
@Controller('home')
export class HomeController {
  // 只有版本 1 有这个方法
  @Get()
  @Version('1')
  findAllV1() {
    return 'find v1';
  }

  // 版本 1 和 2 都有这个方法
  @Post()
  create() {
    return 'create';
  }

  // 只有版本 2 有这个方法
  @Get()
  @Version('2')
  findAllV2() {
    return 'find v2';
  }
}

请求路径

  • POST /v1/home'create'
  • POST /v2/home'create'(兼容版本 1)
  • GET /v1/home'find v1'
  • GET /v2/home'find v2'

4.6 版本控制最佳实践

code
API 版本控制最佳实践:
│
├── 版本命名
│   ├── 使用数字版本:v1、v2、v3
│   ├── 避免使用日期版本
│   └── 避免使用语义化版本(v1.0.0)
│
├── 版本策略
│   ├── 重大变更:发布新版本
│   ├── 小变更:保持兼容
│   └── 废弃版本:提前通知
│
├── 版本迁移
│   ├── 提供迁移指南
│   ├── 设置废弃警告
│   └── 逐步淘汰旧版本
│
└── 文档管理
    ├── 每个版本独立文档
    ├── 标注版本差异
    └── 提供迁移示例

五、完整实战示例

5.1 项目结构

code
src/
├── modules/
│   └── home/
│       ├── home.module.ts
│       ├── home.controller.ts
│       ├── home.service.ts
│       └── dto/
│           ├── create-home-resource.dto.ts
│           └── update-home-resource.dto.ts
├── prisma/
│   └── prisma.service.ts
├── main.ts
└── app.module.ts

5.2 完整控制器示例

src/modules/home/home.controller.ts

typescript
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  Version,
} 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 /v1/home/resources
  @Get('resources')
  @Version('1')
  findAllV1() {
    return this.homeService.findAll();
  }

  // GET /v2/home/resources
  @Get('resources')
  @Version('2')
  findAllV2() {
    return this.homeService.findAllV2();
  }

  // GET /v1/home/resources/:id
  @Get('resources/:id')
  @Version('1')
  findOne(@Param('id') id: string) {
    return this.homeService.findOne(+id);
  }

  // POST /v1/home/resources
  @Post('resources')
  @Version('1')
  create(@Body() createDto: CreateHomeResourceDto) {
    return this.homeService.create(createDto);
  }

  // PUT /v1/home/resources/:id
  @Put('resources/:id')
  @Version('1')
  update(
    @Param('id') id: string,
    @Body() updateDto: UpdateHomeResourceDto,
  ) {
    return this.homeService.update(+id, updateDto);
  }

  // DELETE /v1/home/resources/:id
  @Delete('resources/:id')
  @Version('1')
  remove(@Param('id') id: string) {
    return this.homeService.remove(+id);
  }
}

5.3 完整服务示例

src/modules/home/home.service.ts

typescript
import { Injectable } 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) {}

  // 查询所有资源(版本 1)
  async findAll() {
    return this.prisma.homeResources.findMany({
      orderBy: { order: 'asc' },
    });
  }

  // 查询所有资源(版本 2,增加分页)
  async findAllV2(page: number = 1, limit: number = 10) {
    const skip = (page - 1) * limit;

    const [data, total] = await Promise.all([
      this.prisma.homeResources.findMany({
        skip,
        take: limit,
        orderBy: { order: 'asc' },
      }),
      this.prisma.homeResources.count(),
    ]);

    return {
      data,
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  // 查询单个资源
  async findOne(id: number) {
    return this.prisma.homeResources.findUnique({
      where: { id },
    });
  }

  // 创建资源
  async create(data: CreateHomeResourceDto) {
    return this.prisma.homeResources.create({
      data,
    });
  }

  // 更新资源
  async update(id: number, data: UpdateHomeResourceDto) {
    return this.prisma.homeResources.update({
      where: { id },
      data,
    });
  }

  // 删除资源
  async remove(id: number) {
    return this.prisma.homeResources.delete({
      where: { id },
    });
  }
}

5.4 DTO 定义

src/modules/home/dto/create-home-resource.dto.ts

typescript
import { IsString, IsOptional, IsInt, Min } from 'class-validator';

export class CreateHomeResourceDto {
  @IsString()
  @IsOptional()
  title?: string;

  @IsString()
  @IsOptional()
  subtitle?: string;

  @IsString()
  @IsOptional()
  url?: string;

  @IsString()
  @IsOptional()
  image?: string;

  @IsString()
  @IsOptional()
  desc?: string;

  @IsString()
  @IsOptional()
  module?: string;

  @IsString()
  @IsOptional()
  type?: string;

  @IsString()
  @IsOptional()
  icon?: string;

  @IsInt()
  @Min(0)
  @IsOptional()
  order?: number;
}

src/modules/home/dto/update-home-resource.dto.ts

typescript
import { PartialType } from '@nestjs/mapped-types';
import { CreateHomeResourceDto } from './create-home-resource.dto';

export class UpdateHomeResourceDto extends PartialType(CreateHomeResourceDto) {}

六、请求测试示例

6.1 Postman 测试

测试版本 1 接口

bash
# GET /v1/home/resources
GET http://localhost:3000/v1/home/resources

# Response
[
  {
    "id": 1,
    "title": "测试标题",
    "subtitle": "子标题",
    "url": "https://example.com",
    "image": "https://example.com/image.jpg",
    "desc": "描述",
    "module": "home",
    "type": "banner",
    "icon": null,
    "order": 100,
    "createdAt": "2024-01-01T00:00:00.000Z",
    "updatedAt": "2024-01-01T00:00:00.000Z"
  }
]

测试版本 2 接口

bash
# GET /v2/home/resources?page=1&limit=10
GET http://localhost:3000/v2/home/resources?page=1&limit=10

# Response
{
  "data": [
    {
      "id": 1,
      "title": "测试标题",
      ...
    }
  ],
  "meta": {
    "total": 1,
    "page": 1,
    "limit": 10,
    "totalPages": 1
  }
}

测试创建接口

bash
# POST /v1/home/resources
POST http://localhost:3000/v1/home/resources
Content-Type: application/json

{
  "title": "新资源",
  "subtitle": "新子标题",
  "url": "https://example.com/new",
  "module": "home",
  "type": "image",
  "order": 1
}

# Response
{
  "id": 2,
  "title": "新资源",
  "subtitle": "新子标题",
  ...
}

6.2 curl 命令测试

bash
# 查询所有资源(版本 1)
curl http://localhost:3000/v1/home/resources

# 查询所有资源(版本 2,带分页)
curl "http://localhost:3000/v2/home/resources?page=1&limit=10"

# 创建资源
curl -X POST \
  http://localhost:3000/v1/home/resources \
  -H "Content-Type: application/json" \
  -d '{"title":"新资源","module":"home","type":"image"}'

# 更新资源
curl -X PUT \
  http://localhost:3000/v1/home/resources/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"更新后的标题"}'

# 删除资源
curl -X DELETE http://localhost:3000/v1/home/resources/1

七、最佳实践

7.1 模块化设计

code
模块化设计原则:
│
├── 按功能划分模块
│   ├── 用户模块(UserModule)
│   ├── 课程模块(CourseModule)
│   └── 首页模块(HomeModule)
│
├── 模块职责单一
│   ├── 一个模块一个功能
│   ├── 避免模块过大
│   └── 保持模块独立
│
└── 模块通信
    ├── 通过 Provider 共享服务
    ├── 通过 Module 导入依赖
    └── 避免循环依赖

7.2 RESTful 设计规范

code
RESTful API 设计规范:
│
├── 路径设计
│   ├── 使用名词复数:/users、/posts
│   ├── 使用小写字母
│   ├── 使用连字符:/home-resources
│   └── 避免动词: /getUsers
│
├── HTTP 方法
│   ├── GET:查询资源
│   ├── POST:创建资源
│   ├── PUT:整体更新
│   ├── PATCH:部分更新
│   └── DELETE:删除资源
│
├── 状态码使用
│   ├── 200:成功
│   ├── 201:创建成功
│   ├── 204:删除成功(无返回内容)
│   ├── 400:请求错误
│   ├── 404:资源不存在
│   └── 500:服务器错误
│
└── 响应格式
    ├── 成功:{ data: {...}, message: "成功" }
    └── 失败:{ error: "错误信息", statusCode: 400 }

7.3 版本控制策略

code
API 版本控制策略:
│
├── 何时发布新版本
│   ├── 重大功能变更
│   ├── 接口不兼容变更
│   └── 数据结构变更
│
├── 如何维护多版本
│   ├── 新版本开发新功能
│   ├── 旧版本保持稳定
│   └── 逐步废弃旧版本
│
└── 如何废弃旧版本
    ├── 提前通知(3-6个月)
    ├── 返回警告头
    └── 记录废弃日志

八、学习要点总结

8.1 核心概念速记

code
NestJS 业务接口开发核心概念:
│
├── 数据库迁移
│   ├── 修改 schema.prisma
│   ├── 执行 prisma migrate dev
│   └── 验证数据库变化
│
├── 模块创建
│   ├── nest g module modules/home
│   ├── nest g service modules/home --no-spec
│   └── nest g controller modules/home --no-spec
│
├── RESTful 接口
│   ├── @Get() - 查询
│   ├── @Post() - 创建
│   ├── @Put() - 更新
│   └── @Delete() - 删除
│
└── API 版本控制
    ├── 启用:app.enableVersioning({ type: VersioningType.URI })
    ├── 使用:@Version('1')
    ├── 默认:defaultVersion: '1'
    └── 多版本:defaultVersion: ['1', '2']

8.2 重点知识清单

知识点重要程度掌握程度
数据库迁移流程未掌握 / 已掌握
NestJS CLI 创建模块未掌握 / 已掌握
RESTful 接口规范未掌握 / 已掌握
API 版本控制启用未掌握 / 已掌握
@Version() 装饰器未掌握 / 已掌握
版本控制最佳实践未掌握 / 已掌握
DTO 定义和使用未掌握 / 已掌握

8.3 课后思考题

  1. 如何修改数据库表结构并创建迁移?
  2. 如何使用 NestJS CLI 快速创建模块、服务和控制器?
  3. RESTful 接口规范有哪些?
  4. 如何在 NestJS 中启用 API 版本控制?
  5. @Version() 装饰器有几种使用方式?

参考资料


上一章25-NestJS工作原理与装饰器详解

下一章27-Prisma CRUD操作与高级查询