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 module | nest g mo | 创建模块 | *.module.ts |
nest g service | nest g s | 创建服务 | *.service.ts |
nest g controller | nest g co | 创建控制器 | *.controller.ts |
nest g resource | nest 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/:id3.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.URI | URI 路径版本 | /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.ts5.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 课后思考题
- 如何修改数据库表结构并创建迁移?
- 如何使用 NestJS CLI 快速创建模块、服务和控制器?
- RESTful 接口规范有哪些?
- 如何在 NestJS 中启用 API 版本控制?
- @Version() 装饰器有几种使用方式?
参考资料
- NestJS 官方文档 - Modules
- NestJS 官方文档 - Controllers
- NestJS 官方文档 - Versioning
- Prisma 官方文档 - Migrations
- RESTful API 设计指南