NestJS API 版本控制策略

版本控制是 API 设计的重要组成部分。这也是前期没有给予足够思考的项目方面之一,而且它经常发生在游戏后期,当很难引入重大更改时(并且引入版本控制有时可能是一项重大更改)。在这篇博文中,我们将描述您可以在 NestJS 中实现的各种版本控制策略,特别关注最高匹配的版本选择。当您希望最大限度地减少升级 API 级别版本所需的更改量时,您可能会考虑这种策略。

版本控制类型

在 NestJS 中,可以实现四种不同类型的版本控制:

  • URI 版本控制
    • 版本将在请求的 URI 中传递。例如,如果请求进入/api/v1/users,则v1标记 API 的版本。
    • 这是 NestJS 中的默认设置。
  • 自定义标头版本控制
    • 自定义请求标头将指定版本。例如, 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

接下来,提供FastifyAdapterNestFactory

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分支中。