API versioning and breaking changes

延續上一篇,再補一些心得與讀書筆記,關於 API versioning 和 breaking changes。



(抱歉,我沒想好 AI 咒語,生成了這樣的圖片)

前言

就像開始一個 greenfield project,設計第一版 API 往往是令人興奮與激動的事情。然而,當 API 對外發布之後,開始進入新功能與既有功能交織演進的階段,事情可能會變得複雜許多,因為免不了會出現 breaking changes(重大變更)。


既然有 breaking changes,便得考慮 API 的切版策略。我不確定中文世界裡大家是否會講「切版」,這只是我臨時想到的詞彙。若覺得不習慣,請自行替換成英文的 API versioning。


Breaking change

定義:任何變更若會導致用戶必須作一些因應措施才能讓他們的程式維持正常運行,都稱為 breaking change(重大變更)。


以 Web API 而言,以下都是 breaking changes,沒有疑義:

  • 從 response body 中移除欄位。(用戶端原本有用到此欄位,卻突然出錯了)
  • 變更 request 或 response 中的欄位型別或驗證規則。
  • 在 request 中加入新的必要欄位。


以下變更則大致可歸類為 non-breaking changes,也就是可以相容於既有的用戶端。不過,有些仍可能對用戶端程式造成衝擊。

  • 在 response 中增加欄位。
    用戶端程式通常能夠自動忽略 response 中多餘的欄位,故不至於產生衝擊。

  • 在 request 中增加 optional 欄位。
    由於不是必要欄位,故不會引發 API server 報錯。

  • 從 request 中移除欄位。
    這表示既有用戶端程式會傳入多餘的、已遭廢棄的欄位;API server 通常會自動忽略這些多餘的傳入參數,但仍有出錯的可能。

應對之道

Breaking change 在所難免,常見應對之道有以下三種作法:

  • 協調與通知用戶端與相關人員(不用煩惱 versioning 的一堆技術問題)
  • 多版本並行
  • 增加功能與棄用功能(推薦❤️)

協調與通知

協調與通知用戶端與相關人員:「我們的 API 即將於某月某日更新版本囉!以下是變動細節。」

此作法並不對 Web API 作版本切分,而是在同一份 code base 裡面持續修改 API。主要的成本在於新版發行之前的公告周知與協調。之所以需要協調,正是因為 API 有 breaking changes,而用戶端使用 API 的行為各有不同,必須先知會所有用戶端,協調出一個大家都覺得可以安心更版的時間點。

適用場合:開發團隊知道而且能夠聯繫上所有的用戶端。通常用於公司內部。

如果是公開的 API,那麼每次部署新版本的時候,可能就得公開預告和提醒既有用戶端:「你們的應用程式可能會因為 API 改版而有 downtime。

多版本並行

當 breaking changes 無可避免,API 便需要同時支援多個版本。例如 v1 與 v2,分別代表兩個版本。基於維護成本考量,開發團隊可能只維持幾個最近的版本同時存在,太舊的版本則予以永久下架。

常見的切版方式有以下幾種:
  • 以 URI 的 base path 切分。例如: /v1/employee/.../v2/employee/...
  • 使用查詢參數。例如:/employee?version=1/employee/version=2
  • 使用 header。例如:Version=1Version=2
  • 使用 media types。例如:application/vnd.myapi.v1+jsonapplication/vnd.myapi.v2+json
使用查詢參數、header、或 media types 來切分 API 版本的優點是低成本,且有彈性:程式內部實作既可以擴及整個 API,也可以僅針對個別 operations 來區分版本。缺點是多個版本的設計與實作全都混在同一個 API spec 和 code base,時間一久,可能會越來越混亂,寫給使用者看的教學文件也要加入一堆備註和提醒。

以 URI 的 base path 切分

以 URI 的 base path 來切分版本,則是各個版本各自分開,即各有自己的一份 API spec 和 code base。當然,API 教學文件、使用手冊等等,也是各自維護一份。管理成本不在話下。但也有好處,例如當開發團隊推出 API 的 v2 版本時,v1 的規格與實作可能就宣布進入凍結狀態(或只修重大 bugs),不再添加新功能。此後只專心發展 v2,省得瞻前顧後。

適用 URI base path 切版的場合:即將推出的新版本跟舊版本之間的功能差異較大,大到讓開發團隊覺得還是切出一個新版本比較乾淨、好維護。

至於版本的編號方式,不見得是簡單的 v1 和 v2。比較常見的是採用 Semantic Versioning,即 <major>.<minor>.<patch>。例如 2.3.0。一般作法如下:
  • 有 breaking change 的時候,<major> 編號遞增。
  • 加入新功能、且沒有 breaking change 的時候,<minor> 編號遞增。
  • 修 bugs 只動 <patch> 編號。

增加功能與棄用功能

前面介紹的幾種版本切分作法,開發與維護成本都不低。還有一種更簡單的策略,是對同一個版本的 code base 加入新功能,並且讓一些過時的功能繼續存在一段時間,然後逐漸消失在歷史的灰燼中。這也是比較推薦的作法。

以下範例展示了如何在 OpenAPI spec 中標示某個屬性已被棄用(deprecated),好讓用戶端有時間改用新的方法。

components:
  schemas:
    Address:
      type: object
      properties:
        street:
          type: string
        city:
          type: string
    Person:
      type: object
      properties:
        name:
          type: string
        age:
          type: integer
          description: "已廢棄,請改用..."
          deprecated: true
    Customer:
      allOf:
        - $ref: '#/components/schemas/Person'
        - type: object
          properties:
            address:
              $ref: '#/components/schemas/Address'
            salary:
              type: number

下圖可以看到 Swagger Editor 呈現的預覽頁面:


結語

了解幾種 API versioning 的作法之後,個人覺得以 URI 的 base path 切分(例如 /v1/.../v2/...)搭配新增與棄用功能(標示 deprecated)的做法蠻靈活的,看起來可以應付大部分的變動。比如說,對於一些比較小的變動,僅只是把新功能加入當前版本的 API spec,並且把即將棄用的功能標示為 deprecated。不過,這只是我自己的理解和推想;實際專案的需求變化多端,故這些作法能 cover 多少狀況,以及是否衍生其他問題,也還是要看個別專案和團隊而定吧。

Keep learning! 

參考資料


沒有留言:

技術提供:Blogger.
回頂端⬆️