”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Monite 的 API 版本控制

Monite 的 API 版本控制

发布于2024-11-08
浏览:271

我们都喜欢拥有闪亮的新工具,但讨厌不断更新它们的苦差事。这适用于任何事物:操作系统、应用程序、API、Linux 软件包。当我们的代码因为更新而停止工作时,这是痛苦的,而当更新甚至不是我们发起的时,痛苦会加倍。

在 Web API 开发中,您始终面临着每次新更新都会破坏用户代码的风险。如果你的产品是API,那么每次这些更新都会令人恐惧。 Monite 的主要产品是我们的 API 和白标 SDK。我们是一家 API 优先的公司,因此我们非常注意保持我们的 API 稳定且易于使用。因此,重大变更的问题几乎是我们优先考虑的问题。

一个常见的解决方案是向您的客户发出弃用警告并很少发布重大更改。突然之间,您的发布现在可能需要几个月的时间,并且某些功能必须保持隐藏甚至不合并,直到每个下一个版本为止。这会减慢您的开发速度并迫使您的用户每隔几个月更新一次集成。

如果您加快发布速度,您的用户将不得不过于频繁地更新他们的集成。如果延长发布之间的时间,公司的进展就会变慢。你给用户带来的越不方便,对你来说就越方便,反之亦然。这当然不是最佳方案。我们希望按照自己的节奏前进,而不会对现有客户造成任何破坏,而常规弃用方法是不可能做到这一点的。这就是我们选择替代解决方案的原因:API 版本控制.

这是一个非常简单的想法:随时发布任何重大更改,但将它们隐藏在新的 API 版本下。它为您提供了两全其美的优势:用户的集成不会经常被破坏,并且您将能够以您喜欢的任何速度移动。用户可以随时迁移——没有任何压力。

考虑到这个想法的简单性,它对任何公司来说都是完美的。这就是您期望在典型的工程博客中读到的内容。遗憾的是,事情并没有那么简单。

注意价格

API 版本控制非常困难。一旦你开始实施它,它虚幻的简单性很快就会消失。遗憾的是,互联网从未真正警告过您,因为有关该主题的资源少得惊人。他们中的绝大多数人争论 API 版本应该放在哪里,但只有少数文章试图回答:“我们如何实现它?”。最常见的是:

  • 将同一 Web 应用程序的不同版本放入单独的部署中
  • 复制版本之间已更改的单个路由
  • 复制每个版本的整个版本化应用程序

单独的部署可能会变得非常昂贵且难以支持,复制单个路由不能很好地扩展到大型更改,并且复制整个应用程序会创建大量额外的代码,以至于在几个版本之后您就会开始淹没其中。

即使您尝试选择最便宜的,版本控制的负担也会很快赶上。一开始,它会感觉很简单:在这里添加另一个模式,在那里添加业务逻辑的另一个分支,并在最后复制一些路由。但是,如果版本足够多,您的业务逻辑将很快变得难以管理,许多开发人员会弄错应用程序版本和 API 版本,并开始对数据库中的数据进行版本控制,您的应用程序将变得无法维护。

您可能希望永远不会同时拥有超过两个或三个 API 版本;您每隔几个月就可以删除旧版本。如果您只支持少数内部消费者,确实如此。但您组织之外的客户不会享受每隔几个月就被迫升级的体验。

API 版本控制很快就会成为基础设施中最昂贵的部分之一,因此提前进行深入研究至关重要。如果您只支持内部消费者,那么使用 GraphQL 等工具可能会更简单,但它很快就会变得与版本控制一样昂贵。

如果您是一家初创公司,明智的做法是将 API 版本控制推迟到开发的后期阶段,此时您有资源来正确执行此操作。在那之前,弃用和附加变更策略可能就足够了。您的 API 并不总是看起来很棒,但至少您可以通过避免显式版本控制来节省大量资金。

我们如何实现 API 版本控制?

经过几次尝试和许多错误后,我们处于十字路口:我们上面提到的先前版本控制方法的维护成本太高。经过我们的努力,我设计了以下完美版本控制框架所需的要求列表:

  1. 维护大量版本很容易”以确保版本控制不会减慢我们的功能开发速度
  2. 删除旧版本很容易”以确保我们可以毫不费力地清理我们的代码库
  3. 创建新版本不太容易”以确保我们的开发人员仍然有动力尝试解决没有版本的问题。
  4. 维护版本之间的变更日志很容易”以确保我们和我们的客户始终能够确定版本之间的真正差异

遗憾的是,我们现有的方法几乎没有其他选择。这时我想到了一个疯狂的想法:如果我们尝试构建一些复杂的、适合工作的东西——比如 Stripe 的 API 版本控制,会怎么样?

经过无数次实验,我们现在拥有了 Cadwyn:一个开源 API 版本控制框架,它不仅实现了 Stripe 的方法,而且显着构建于其之上。我们将讨论它的 Fastapi 和 Pydantic 实现,但核心原则与语言和框架无关。

凯德温的工作原理

版本变更

所有其他版本控制方法的问题是我们重复太多。当我们的合约只有一小部分被破坏时,为什么我们要复制整个路由、控制器甚至应用程序?

借助 Cadwyn,每当 API 维护者需要创建新版本时,他们都会将重大更改应用于最新的架构、模型和业务逻辑。然后他们创建一个版本更改——一个封装新版本和先前版本之间所有差异的类。

例如,假设以前我们的客户可以创建一个具有地址的用户,但现在我们希望允许他们指定多个地址而不是单个地址。版本更改如下所示:

class ChangeUserAddressToAList(VersionChange):
    description = (
        "Renamed `User.address` to `User.addresses` and "
        "changed its type to an array of strings"
    )
    instructions_to_migrate_to_previous_version = (
        schema(User).field("addresses").didnt_exist,
        schema(User).field("address").existed_as(type=str),
    )

    @convert_request_to_next_version_for(UserCreateRequest)
    def change_address_to_multiple_items(request):
        request.body["addresses"] = [request.body.pop("address")]

    @convert_response_to_previous_version_for(UserResource)
    def change_addresses_to_single_item(response):
        response.body["address"] = response.body.pop("addresses")[0]
Cadwyn 使用

instructions_to_migrate_to_previous_version 为较旧的 API 版本的模式生成代码,这两个转换器函数是让我们能够维护任意数量版本的技巧。该过程如下所示:

  1. Cadwyn 使用change_address_to_multiple_items 转换器将所有用户请求从较旧的 API 版本转换为最新的 API 版本,并将它们传送到我们的业务逻辑
  2. 业务逻辑、其 API 响应和数据库模型始终根据最新的 API 版本进行定制(当然,即使在新版本中删除了旧功能,它仍然必须支持旧功能)
  3. 业务逻辑生成响应后,Cadwyn 使用change_addresses_to_single_item 转换器将其转换为客户端请求者当前使用的旧 API 版本。

我们的 API 维护人员创建版本更改后,他们需要将其添加到我们的 VersionBundle 中,以告诉 Cadwyn 此 VersionChange 将包含在某个版本中:

VersionBundle(
    Version(
        date(2023, 4, 27),
        ChangeUserAddressToAList
    ),
    Version(
        date(2023, 4, 12),
        CollapseUserAvatarInfoIntoAnID,
        MakeUserSurnameRequired,
    ),
    Version(date(2023, 3, 15)),
)

就是这样:我们添加了一项重大更改,但我们的业务逻辑仅处理单个版本 - 最新版本。即使我们添加了数十个 API 版本,我们的业务逻辑仍然不受版本控制逻辑、不断重命名、if 和数据转换器的影响。

版本链接

版本更改取决于 API 的公共接口,我们几乎从不向现有 API 版本添加重大更改。这意味着一旦我们发布了该版本,它就不会被破坏。

因为版本更改描述了版本内的重大更改,并且旧版本中没有重大更改,所以我们可以确定我们的版本更改是完全不可变的 - 它们永远不会有理由更改。不可变实体比业务逻辑的一部分更容易维护,因为它总是在发展。版本更改也会被一个接一个地应用——在版本之间形成一系列转换器,可以将任何请求迁移到任何较新版本,并将任何响应迁移到任何旧版本。

API Versioning at Monite

副作用

API 合约比模式和字段复杂得多。它们由所有端点、状态代码、错误、错误消息,甚至业务逻辑行为组成。 Cadwyn 使用我们上面描述的相同 DSL 来处理端点和状态代码,但错误和业务逻辑行为是另一回事:它们不可能使用 DSL 进行描述,它们需要嵌入到业务逻辑中。

这使得此类版本更改的维护成本比所有其他版本更改都要昂贵得多,因为它们会影响业务逻辑。我们将此属性称为“副作用”,并且由于其维护负担,我们会不惜一切代价避免它们。所有想要修改业务逻辑的版本更改都需要标记为具有副作用。它将作为一种了解哪些版本更改是“危险”的方法:

class RequireCompanyAttachedForPayment(VersionChangeWithSideEffects):
    description = (
        "User must now have a company_id in their account "
        "if they want to make new payments"
    )

它还允许 API 维护者检查客户端请求是否使用包含此副作用的 API 版本:

if RequireCompanyToBeAttachedForPayment.is_applied:
    validate_company_id_is_attached(user)

没有银弹

Cadwyn 有很多好处:它大大减轻了我们开发人员的负担,并且可以集成到我们的基础设施中以自动生成变更日志并改进我们的 API 文档。

然而,版本控制的负担仍然存在,即使是复杂的框架也不是灵丹妙药。我们尽力仅在绝对必要时才使用 API 版本控制。我们还通过设立一个特殊的“API 委员会”来尝试使我们的 API 在第一次尝试时就正确。在任何实施开始之前,所有重要的 API 更改都会由我们最好的开发人员、测试人员和技术编写人员进行审核。

特别感谢 Brandur Leach 在 Stripe 上发表的 API 版本控制文章,以及他在我实现 Cadwyn 时向我提供的帮助:没有他的帮助,这是不可能的。

版本声明 本文转载于:https://dev.to/monite/api-versioning-at-monite-3ba3?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何在 Bigquery 参数化查询中传递结构数组
    如何在 Bigquery 参数化查询中传递结构数组
    在Google的Bigquery中,SQL查询可以参数化。如果您不熟悉这个概念,它基本上意味着您可以将 SQL 查询编写为参数化模板,如下所示: INSERT INTO mydataset.mytable(columnA, columnB) VALUES (@valueA, @valueB)...
    编程 发布于2024-11-08
  • 如何使用 Python“for”循环实现 C/C++ 风格循环?
    如何使用 Python“for”循环实现 C/C++ 风格循环?
    在 Python 中实现 C/C 风格循环:“for”循环在 Python 中,循环提供了一种用于迭代序列的通用机制。虽然 Python 的“for”循环语法与其 C/C 对应部分不同,但实现类似的功能仍然是可行的。考虑 C/C 中的以下循环:for(int k = 1; k <= c; k ...
    编程 发布于2024-11-08
  • Laravel 入门:查询生成器初学者指南
    Laravel 入门:查询生成器初学者指南
    Laravel 的 查询生成器 提供了一个强大、流畅的界面,用于在 PHP 中构建 SQL 查询。它允许您以富有表现力的、类似 SQL 的语法与数据库交互,同时抽象出大部分复杂性。 我们将演练 Laravel 应用程序中的典型用例,使用查询生成器执行各种任务,例如选择、插入、更新和删除数据。 ...
    编程 发布于2024-11-08
  • 如何截断长分页列表以增强用户体验?
    如何截断长分页列表以增强用户体验?
    截断长页面列表以实现高效分页分页是任何显示大量数据的网站或应用程序的重要组成部分,因为它允许用户以可管理的块的方式浏览它。但是,如果以简单的方式实现,分页可能会导致页面列表过长,特别是当应用于具有大量页面的数据集时。为了缓解此问题,有必要截断这些页面列表为用户提供更简洁的导航选项。在本文中,我们将深...
    编程 发布于2024-11-08
  • 如何在 JavaScript 中展平数组
    如何在 JavaScript 中展平数组
    使用递归和 while 循环是更简单的方法之一 export default function flatten(value) { const arr = [] const flat = (a) => { let counter = 0 console.log(a) ...
    编程 发布于2024-11-08
  • 为什么我无法在 Powershell 中运行复杂的 ImageMagick 命令,但它们可以在 CMD 中运行?
    为什么我无法在 Powershell 中运行复杂的 ImageMagick 命令,但它们可以在 CMD 中运行?
    ImageMagick 命令无法在 Powershell 中运行,但在 cmd 窗口中运行没有问题在尝试使用 ImageMagick 命令时,用户在 Powershell 窗口中执行它们时遇到了挑战。这些命令在 cmd 窗口中无缝运行。尝试通过在括号前添加反斜杠来解决该问题也没有成功。调查显示,ma...
    编程 发布于2024-11-08
  • 什么时候可以从 C++ 标准库类继承?
    什么时候可以从 C++ 标准库类继承?
    通过继承扩展 C 标准库虽然人们通常认为从 C 标准库类继承是不可取的,但也有一些值得注意的例外.可识别的类继承确定标准库类是否用于继承可能具有挑战性。但是,以下准则可以提供一些见解:如果类具有虚方法,则它可能是继承的候选者。过多的“friend”声明表明存在封装问题,从而导致继承不太合适。模板应该...
    编程 发布于2024-11-08
  • 利用 AI 快速学习 Node.js - 第 2 天
    利用 AI 快速学习 Node.js - 第 2 天
    今天,我借助AI继续我的Node.js学习之旅,第2天的主题是Node.js中的模块系统。由于我已经熟悉 JavaScript,因此了解这种语言如何将代码组织成模块,从而使其更易于构建和重用是很有趣的。 理论部分:Node.js 中的模块基础知识 首先,我完成了理论部分,其中解释了两...
    编程 发布于2024-11-08
  • 优化 Next.js 应用性能的经过验证的技巧 ⚡️
    优化 Next.js 应用性能的经过验证的技巧 ⚡️
    优化 Web 应用程序的性能对于提供快速、流畅的用户体验至关重要。 借助 Next.js 这个强大的 React 框架,您可以利用许多内置功能来提高应用程序的速度和效率。 以下是让 Next.js 应用获得最佳性能的十个关键策略: 1. 仅加载您需要的 JavaScript 和 ...
    编程 发布于2024-11-08
  • 为什么 Python 和 Golang Zlib 产生不同的压缩输出?
    为什么 Python 和 Golang Zlib 产生不同的压缩输出?
    了解 Golang 和 Python Zlib 输出的差异使用 Zlib 压缩来压缩字符串时,Python 的 zlib 库会产生与Golang 的 zlib 实现。具体来说,第五个字节不同,Python 的值为 0,而 Golang 的值为 4。差异原因输出的差异源于来自 Python 和 Go ...
    编程 发布于2024-11-08
  • 如何在PHP中按扩展名高效过滤文件?
    如何在PHP中按扩展名高效过滤文件?
    在 PHP 中按扩展名高效过滤文件您希望根据文件扩展名过滤目录中的文件,特别是 . ini 文件。虽然 scandir() 提供目录中所有文件的列表,但它并不是过滤检索的最有效方法。利用 PHP 的 glob() 函数高效获取具有特定扩展名的文件,利用 PHP 的 glob() 函数。此函数采用模式...
    编程 发布于2024-11-08
  • FiveM x TypeScript
    FiveM x TypeScript
    FiveM 是 Grand Theft Auto V 的修改版,使您能够在由 Cfx.re 提供支持的定制专用服务器上玩多人游戏。 当您开发 FiveM 服务器时,您可以创建资源。这些资源可以用多种语言编写:Lua、C# 和 JavaScript。在本文中,我们将了解如何使用 TypeScript...
    编程 发布于2024-11-08
  • 完整指南:在 Telegram 和 MetaTraderm Python 之间构建安全集成机器人
    完整指南:在 Telegram 和 MetaTraderm Python 之间构建安全集成机器人
    Bem-vindo! Este guia é projetado desenvolvedores intusiastas em Python que desejam criar um bot que monitora mensagens no Telegram e interage com o Me...
    编程 发布于2024-11-08
  • 如何使用 LOCK TABLE 识别 MySQL 中锁定的表?
    如何使用 LOCK TABLE 识别 MySQL 中锁定的表?
    通过 LOCK TABLE 识别 MySQL 中锁定的表使用 LOCK TABLE 命令确定 MySQL 中哪些表被锁定对于数据库管理至关重要。了解哪些表受到影响使管理员能够解决冲突、减少停机时间并促进顺利的数据库操作。解决方案:利用 SHOW OPEN TABLESMySQL 提供了一个强大的命令...
    编程 发布于2024-11-08
  • Go 中的“new()”函数和“&”内存地址运算符有什么区别?
    Go 中的“new()”函数和“&”内存地址运算符有什么区别?
    new()和“&”运算符的区别Go中new()函数和“&”内存地址运算符都用于分配内存。然而,它们的用法之间存在一些细微的差异。功能比较new()和&运算符都返回指向新分配的内存地址的指针。但是,它们的语法有所不同:v := &Vector{} 使用“&”运算符获取空 Vector 结构体的地址。v...
    编程 发布于2024-11-08

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3