当然!我们来聊聊一个当你的应用开始成长、迭代时,会遇到的一个至关重要的话题:API 版本控制 (Versioning)。这就像是为你的应用安装了一台“时光机”,让它能够在不断进化的同时,还能与过去的用户和平共处。
API 的“时光机”:从容应对变化的 NestJS 版本控制
想象你的应用是一座正在蓬勃发展的城市。
- V1 版本 是城市的“历史老城区”。这里的道路(API 路由)和建筑(数据结构)已经建成,很多老居民(现有的移动 App 或前端应用)已经非常熟悉这里的布局,每天都在使用。
- 随着城市发展,你决定规划一片“现代新区”(V2 版本)。这里有更宽阔的马路(新的路由)、更现代的摩天大楼(新的数据结构),甚至有些老城区的建筑在这里被彻底翻新或拆除。
问题来了:你在建设新城区的过程中,总不能把老城区的路给堵上,或者把老居民的家给拆了吧?这样做会引起巨大的混乱(所有依赖旧版 API 的应用全部崩溃)。
API 版本控制 就是城市规划中的“分区”策略。它允许你的“历史老城区 (V1)”和“现代新区 (V2)”同时存在,和平共存。老居民可以继续走他们熟悉的老路,新居民可以享受新区的便利。这让你的应用可以平滑地、无破坏性地进行升级和迭代。
NestJS 提供了几种内置的、非常优雅的方式来实现 API 版本控制。
1. 第一步:启动“时光机” (在 main.ts
中开启版本控制)
无论你选择哪种版本控制策略,第一步总是在 main.ts
中开启版本控制功能。
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common'; // 导入版本控制类型
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 就像给城市规划局颁发指令,开启分区规划!
app.enableVersioning({
// 在这里选择你的版本控制策略
type: VersioningType.URI,
});
await app.listen(3000);
}
bootstrap();
app.enableVersioning()
就是启动“时光机”的开关。关键在于 type
这个选项,它决定了我们如何区分不同的版本。NestJS 提供了几种主要的“时空道标”:
2. 三种主要的“时空道标”
第一站:URI 版本控制 (在 URL 里逛‘老城区’)
- 策略:
VersioningType.URI
- 工作方式: 将版本号直接作为 URL 的一部分。这是最直观、最明确的方式。
- 城市比喻: 就像街道地址明确包含了区域信息:
GET /老城区/v1/市政厅
vs.GET /新城区/v2/市民中心
。
如何使用:
- 在
main.ts
中设置type: VersioningType.URI
。 - 在你的 Controller 或路由处理函数上,使用
@Version()
装饰器来标记它属于哪个版本。
src/cats/cats.controller.ts
import { Controller, Get, Version } from '@nestjs/common';
@Controller('cats')
export class CatsController {
// 这个路由属于 V1 版本
@Version('1')
@Get()
findAllV1() {
return 'This is version 1 of all cats.';
}
// 这个路由属于 V2 版本,它可能返回了更丰富的数据结构
@Version('2')
@Get()
findAllV2() {
return { data: ['Cat A', 'Cat B'], version: 2 };
}
}
如何访问:
- 请求
GET /v1/cats
-> NestJS 会自动匹配到findAllV1()
方法,返回"This is version 1 of all cats."
。 - 请求
GET /v2/cats
-> NestJS 会匹配到findAllV2()
方法,返回一个 JSON 对象。
默认情况下,版本前缀是 v
。你可以在 enableVersioning
中修改它:defaultVersion: '1', prefix: 'version/'
。
第二站:Header 版本控制 (凭“通行证”进入不同区域)
- 策略:
VersioningType.HEADER
- 工作方式: URL 保持不变,但客户端需要在 HTTP 请求头 (Header) 中指定一个版本号。
- 城市比喻: 城市的入口只有一个 (
/cats
),但你需要向门卫出示不同版本的“通行证”(Header),他才会引导你去往不同的区域。
如何使用:
- 在
main.ts
中设置type: VersioningType.HEADER
,并指定 header 的名称:header: 'X-Api-Version'
。 - Controller 中的代码和 URI 版本控制完全一样,使用
@Version()
装饰器。
如何访问:
curl http://localhost:3000/cats -H "X-Api-Version: 1"
-> 访问findAllV1()
。curl http://localhost:3000/cats -H "X-Api-Version: 2"
-> 访问findAllV2()
。- 如果不提供
X-Api-Version
Header,会收到一个404 Not Found
错误。
这种方式的好处是保持了 URL 的整洁,但对客户端的要求更高一些。
第三站:Media Type 版本控制 (根据“导游手册”的版本)
- 策略:
VersioningType.MEDIA_TYPE
- 工作方式: 版本信息作为
Accept
Header 的一部分被发送,遵循application/json;v=1
这样的格式。 - 城市比喻: 客户端(游客)在请求时,明确表示“我需要一份第一版的导游手册 (Accept Header)”,服务器(旅游局)就会提供第一版的信息。
如何使用:
- 在
main.ts
中设置type: VersioningType.MEDIA_TYPE
,并指定 key:key: 'v='
。 - Controller 代码依然不变。
如何访问:
curl http://localhost:3000/cats -H "Accept: application/json;v=1"
-> 访问findAllV1()
。curl http://localhost:3000/cats -H "Accept: application/json;v=2"
-> 访问findAllV2()
。
这是最符合 RESTful 理念的一种方式,但也是最复杂的,通常用于大型、非常正式的公共 API。
3. “时空旅行”的高级技巧
一个路由,多个版本
如果某个路由在 V1 和 V2 版本的行为完全一样,你不需要写两遍。
@Version(['1', '2']) // 这个路由同时服务于 V1 和 V2
@Get('legacy')
findLegacyData() {
return 'This data is available in both v1 and v2.';
}
版本中立的路由
如果一个路由(比如健康检查 /health
)不属于任何版本,或者应该对所有版本的请求都可用,你可以将它标记为“中立”。
import { VERSION_NEUTRAL } from '@nestjs/common';
@Version(VERSION_NEUTRAL)
@Get('health')
checkHealth() {
return { status: 'ok' };
}
现在,无论你用 V1 还是 V2 的方式去请求 /health
,都能得到响应。
整个 Controller 的版本控制
你可以直接在 @Controller
装饰器上标记版本,它会应用到该控制器下的所有路由。
@Controller('users')
@Version('1') // 这个控制器下的所有路由都默认是 V1
export class UsersV1Controller {
@Get() // 自动成为 GET /v1/users
findAll() {
/* ... */
}
}
总结
API 版本控制是专业后端开发者的必备技能,它能让你的应用在迭代中保持向后兼容性。
策略 | VersioningType | 客户端如何指定版本 | 优点 | 缺点 |
---|---|---|---|---|
URI | .URI | GET /v1/cats | 最直观,易于理解和调试。 | 可能会让 URL 变长。 |
Header | .HEADER | Header: 'X-Api-Version: 1' | 保持 URL 整洁。 | 不如 URI 直观,需要客户端配合。 |
Media Type | .MEDIA_TYPE | Header: 'Accept: ...;v=1' | 最符合 RESTful 规范。 | 最复杂,对客户端要求最高。 |
对于大多数应用,URI 版本控制 是一个非常好的起点,因为它清晰、明确且易于实现。选择适合你项目需求的“时空道标”,然后开始放心地建设你的“现代新区”,而不用担心打扰到“历史老城区”的老居民们吧!