NestJS API 版本控制策略
版本控制是 API 设计的重要组成部分。这也是前期没有给予足够思考的项目方面之一,而且它经常发生在游戏后期,当很难引入重大更改时(并且引入版本控制有时可能是一项重大更改)。在这篇博文中,我们将描述您可以在 NestJS 中实现的各种版本控制策略,特别关注最高匹配的版本选择。当您希望最大限度地减少升级 API 级别版本所需的更改量时,您可能会考虑这种策略。
版本控制类型
在 NestJS 中,可以实现四种不同类型的版本控制:
- URI 版本控制
- 版本将在请求的 URI 中传递。例如,如果请求进入
/api/v1/users
,则v1
标记 API 的版本。 - 这是 NestJS 中的默认设置。
- 版本将在请求的 URI 中传递。例如,如果请求进入
- 自定义标头版本控制
- 自定义请求标头将指定版本。例如,
X-API-Version: 1
在一个请求/api/users
中将请求 v1 版本的 API。
- 自定义请求标头将指定版本。例如,
- 媒体类型版本控制
- 与自定义标头版本控制类似,标头将指定版本。只是,这一次,使用了标准的媒体接受头。例如:
Accept: application/json;v=2
- 与自定义标头版本控制类似,标头将指定版本。只是,这一次,使用了标准的媒体接受头。例如:
- 自定义版本控制
- 请求的任何方面都可以用来指定版本。提供了一个自定义函数来提取所述版本。
- 例如,您可以使用此机制实现查询参数版本控制。
URI 版本控制和自定义标头版本控制是实现版本控制时最常见的选择。
在决定要使用哪种类型的版本控制之前,定义版本控制策略也很重要。您想在 API 级别上进行版本控制吗?还是在端点级别?
如果您想使用端点版本控制方法,这可以让您对端点进行更细粒度的控制,而无需还原整个 API。这种方法的缺点是可能难以跟踪端点版本。API 客户端如何知道哪个版本是最新的,或者哪些端点相互兼容?为此需要一个发现机制,或者只是维护良好的文档。
不过,API 级别的版本控制更为常见。使用 API 级别的版本控制,每次引入重大更改时,都会交付整个 API 的新版本,即使在内部,大部分代码都没有改变。有一些策略可以缓解这种情况,我们将在这篇博文中特别关注一个。但首先,让我们看看如何在 API 上启用版本控制。
将版本应用到您的端点
第一步是在 NestJS 应用程序上启用版本控制:
app.enableVersioning({
type: VersioningType.URI,
});
启用 URI 版本控制后,要在端点上应用版本,您可以在@Controller
装饰器上提供版本以将版本应用到控制器下的所有端点,或者将版本应用到带有装饰器的控制器中的路由@Version
.
在下面的示例中,我们对findAll()
方法使用端点版本控制。
import { Controller, Get, Param, Version } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@Version('1')
findAll() {
return 'findAll()';
}
@Get(':id')
findOne(@Param('id') id: string) {
return `findOne(${id})`;
}
}
我们可以findAll()
使用curl调用:
➜ nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/v1/users
findAll()%
但是,我们如何调用findOne()
?由于 onlyfindAll()
是版本化的,因此调用findOne()
需要没有版本。当您请求没有版本的端点时,NestJS 将尝试查找所谓的“版本中立”端点,即没有任何版本注释的端点。
在我们的例子中,这意味着我们使用的 URI 不会v1
在路径中包含或任何其他版本:
➜ nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/users/1
findOne(1)%
发生这种情况是因为如果 API 客户端未请求任何版本,NestJS 会隐含地将“版本中性”版本视为默认版本。 默认版本是应用于所有没有通过装饰器指定版本的控制器/路由的版本。我们之前编写的版本控制配置可以很容易地写成:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: VERSION_NEUTRAL,
});
意思是,任何没有版本的控制器/路由(例如findAll()
上面),默认情况下都将被赋予“版本中性”版本。
如果我们不想使用与版本无关的端点,那么我们可以指定一些其他版本作为默认版本。
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
端点现在将findOne()
返回 404,除非您使用显式版本调用它。这是因为我们不再在任何地方(控制器/路由或defaultVersion
属性)定义任何“版本中立”版本。
➜ nestjs-versioning-strategies git:(main) ✗ curl http://localhost:3000/api/users/1
{"statusCode":404,"message":"Cannot GET /api/users/1","error":"Not Found"}%
多个版本
通过将版本设置为数组,可以将多个版本应用于控制器/路由。
import { Controller, Get, Param, Version } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@Version(['1', '2'])
findAll() {
return 'findAll()';
}
@Get(':id')
findOne(@Param('id') id: string) {
return `findOne(${id})`;
}
}
调用/api/v1/users
或/api/v2/users
都将登陆findAll()
控制器中的相同方法。
也可以在defaultVersion
版本控制配置中设置多个版本:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: ['1', '2'],
});
这仅仅意味着没有版本装饰器的控制器/路由将应用于版本 1 和版本 2。
选择最高匹配版本
想象以下场景:您已决定使用 API 级别的版本控制,但您不想在每次增加 API 版本时更新所有控制器/路由。您只想对那些发生重大变化的人执行此操作。其他控制器/路由应保持当前的任何版本。
目前,在 NestJS 中,仅通过配置选项无法完成此操作。但幸运的是,版本控制配置允许您定义自定义版本提取器。版本提取器只是一个函数,它会按照优先顺序告诉 NestJS客户端正在请求哪些版本。例如,如果版本提取器返回一个数组,例如['3', '2', '1']
. 这意味着客户端正在请求版本 3,或者如果 3 不可用,则请求版本 2,或者如果 2 和 3 都不可用,则请求版本 1。
不过,这种匹配度最高的版本选择确实有一个警告。它不能可靠地与 Express 服务器一起使用,因此我们需要切换到 Fastify 服务器。幸运的是,这在 NestJS 中很容易。首先安装 Fastify 适配器:
npm i --save @nestjs/platform-fastify
接下来,提供FastifyAdapter
给NestFactory
:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
import { FastifyAdapter } from '@nestjs/platform-fastify';
async function bootstrap() {
const app = await NestFactory.create(AppModule, new FastifyAdapter());
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
await app.listen(3000);
}
bootstrap();
就是这样。现在我们可以继续编写版本提取器:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { FastifyRequest } from 'fastify';
const DEFAULT_VERSION = '1';
const extractor = (request: FastifyRequest): string | string[] => {
const requestedVersion =
<string>request.headers['x-api-version'] ?? DEFAULT_VERSION;
// If requested version is N, then this generates an array like: ['N', 'N-1', 'N-2', ... , '1']
return Array.from(
{ length: parseInt(requestedVersion) },
(_, i) => `${i + 1}`,
).reverse();
};
async function bootstrap() {
const app = await NestFactory.create(AppModule, new FastifyAdapter());
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor,
defaultVersion: DEFAULT_VERSION,
});
await app.listen(3000);
}
bootstrap();
版本提取器使用x-api-version
标头提取请求的版本,然后返回所有可能版本的数组,包括请求的版本。我们在这个示例中选择使用基于标头的版本控制的原因是,使用版本提取器实现基于 URI 的版本控制太复杂了。
首先,版本提取器获取一个FastifyRequest
. 此实例不提供用于获取部分 URL 的任何属性或方法。您只能在request.url
属性中获取 URL 路径。如果要提取路由令牌或查询参数,则需要自己解析。其次,您还需要根据请求的版本处理路由。
现在,如果我们向控制器添加多个版本,我们将始终获得支持的最高版本:
import { Controller, Get, Param, Version } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@Version('2')
findAll2() {
return 'findAll2()';
}
@Get()
@Version('1')
findAll1() {
return 'findAll1()';
}
@Get(':id')
findOne(@Param('id') id: string) {
return `findOne(${id})`;
}
}
让我们测试一下:
➜ ~ curl http://localhost:3000/api/users/1 --header "X-Api-Version: 1"
findOne(1)%
➜ ~ curl http://localhost:3000/api/users/1 --header "X-Api-Version: 2"
findOne(1)%
➜ ~ curl http://localhost:3000/api/users --header "X-Api-Version: 2"
findAll2()%
➜ ~ curl http://localhost:3000/api/users --header "X-Api-Version: 1"
findAll1()%
我们只有一个findOne()
实现,它没有应用任何显式版本。但是,由于默认版本是 1(在版本控制配置中配置),这意味着版本 1 适用于findOne()
端点。现在,如果客户端请求我们 API 的版本 2,版本提取器将告诉 NestJS 如果存在则首先尝试版本 2 的端点,或者如果不存在则尝试版本 1。
与 不同findOne()
,findAll1()
并且findAll2()
应用了显式版本:分别为版本 1 和版本 2。这就是为什么第三次和第四次调用将返回客户端明确请求的版本。
结论
这是您可以使用的用于在 NestJS 中实现各种版本控制策略的工具的概述,特别关注 API 级别的版本控制和最高匹配的版本选择。如您所见,NestJS 提供了一种非常健壮的方式来实现各种策略。但是有些带有警告,因此在决定在项目中使用哪种版本控制策略之前,最好提前了解它们。
这个迷你项目的完整源代码在 GitHub 上可用,与最高匹配版本实现相关的代码在highest-matching-version-selection
分支中。