天天看點

淺析 Open API 設計規範

背景

最近由于業務需求,我參與研發的雲産品 CSB 需要對外開放 Open API,原本不是什麼難事,因為阿裡雲内部的 Open API 開放機制已經非常成熟了,根本不需要我去設計,但這次的需求主要是針對一些獨立部署的場景,需要自行設計一套規範,那就意味着,需要對 Open API 進行一些規範限制了,遂有此文。

Open API 和前端頁面一樣,一直都是産品的門面, Open API 不規範,會拉低産品的專業性。在雲場景下,很多使用者會選擇自建門戶,對接雲産品的  Open API,這對我們提出的訴求便是建構一套成熟的 Open API 機制。

站在業務角度,有一些指導原則,指導我們完善 Open API 機制:

  • 前端頁面使用的接口和 Open API 提供的接口是同一套接口
  • 任意的前端頁面接口都應該有對應的 Open API

站在技術角度,有很多的 API 開放标準可供我們參考,一些開源産品的 Open API 文檔也都非常完善。一方面,我會取其精華,另一方面,要考慮自身産品輸出形态的特殊性。本文将圍繞諸多因素,嘗試探讨出一份合适的 Open API 開放規範。

Open API 設計考慮因素

一個完善的 Open API 規範到底應該規範哪些東西?

站在設計角度,需要考慮:命名規範,構成規範,路徑規範,出入參規範,資料類型規範,統一傳回值規範,錯誤碼規範,分頁規範。

站在團隊角度,團隊中的後端初級中級開發以及前端研發是否有足夠的經驗,領悟并落地好制定的 API 規範。同時,伴随着人員流動,這份 Open API 規範是否可以很好地被傳承下去。

站在行業角度,需要考慮提供 Open API 的産品所在的市場是否已經成熟,API 風格可能已經有了對應的規範。

站在産品角度,每個産品适合的 API 風格是不同的,下文會着重探讨這一角度。

總之,Open API 的設計是很難形成定論的一個東西,我在介紹自身産品最終采用的 Open API 規範之前,會先來聊一下大家耳熟能詳的一些概念,例如 restful。

restful 規範之争

有人的地方就會有江湖。

有代碼的地方也是如此。

如果你在碼圈混,一定聽說過 restful 規範:

  • 增删改查應分别聲明為:POST、DELETE、PUT、PATCH、GET
  • 不應該出現動詞,動詞統一由 HTTP Method 表示
  • 展現出“資源”的抽象
  • 利用 pathVariable,queryParam,header,statusCode 表達很多業務語義

restful 規範看似美好,但如果你真正嘗試過落地,一定會遇到一些類似的問題:

  • 以使用者登入接口為例,此類接口難以映射到資源的增删改查
  • 以查詢最近 7 個小時内的接口請求錯誤率為例,衍生到諸如 graphQL 這類複雜的查詢場景,往往需要 json 結構,GET 是無法實作這一點的,隻有 POST 才可以傳遞

基于此,restful 規範逐漸有了反對的聲音:

  • 強行讓所有的事物都“資源”化一下,有悖于開發常識,接口不一定都能夠通過簡單的增删改查來映射
  • 複雜的查詢語義不一定能夠用 GET 表達

restful 風格的擁趸者,不乏對這些反對言論進行抨擊,社群中不免有“拒絕 restful 風格的主要是低水準不思進取的架構師和前後端程式員們,不會設計是人的問題,不是規範的問題”此類的言論。同時對 restful 進行了升華:複雜參數的檢索問題,在 restful 語義中本就應當歸類為 post,因為該行為并不是對資源的定位(GET),而是對資源的檢索(POST)

這顯然刺激了 restful 風格反對者的神經,不屑道:呵,愚蠢的 restful 原教旨主義者呀。

不知道你是 restful 的擁趸者還是反對者?亦或是,中立者。

restful 之争暫時到此為止,這番争論純屬虛構,看官不必計較。無論你如何看待 restful,下面我的論述,你都可以作為一個中立者,否則效果減半。

ROA 與 RPC

API 設計并不隻有 restful 一種規範,在更大的視角中,主流的 API 設計風格其實可以分為

  • 面向資源的設計,即 ROA(Resource oriented architecture)
  • 面向過程的設計,即 RPC(Remote Procedure Call)

restful 便是 ROA 風格的典型例子,而 RPC 風格則相對而言不太容易被大家熟知,但實際上可能大多數的系統的接口是 RPC 風格的,隻不過 RPC 風格這個概念不太為人所知。

以使用者子產品的 CRUD 為例,對比下兩個風格:

ROA 風格

建立使用者(POST)

Request:
POST /users

{"name": "kirito", "age": 18}

Response:
HTTP 201 Created

{"id": 1, "name": "kirito", "age": 18}           

複制

查詢使用者(GET)

Request:
GET /users/1

Response:
HTTP 200 OK

{"id": 1, "name": "kirito", "age": 18}           

複制

查詢使用者清單(GET)

Request:
GET /users

Response:
HTTP 200 OK

{[{"id": 1, "name": "kirito", "age": 18}], "next": "/users?offset=1"}           

複制

建立/修改使用者(PUT)

Request:
PUT /users/1

{"name": "kirito", "age": 19}

Response:
HTTP 200 OK

{"id": 1, "name": "kirito", "age": 19}           

複制

修改使用者(PATCH)

Request:
PATCH /users/1

{"age": 20}

Response:
HTTP 200 OK

{"id": 1, "name": "kirito", "age": 20}           

複制

删除使用者(DELETE)

Request:
DELETE /users/1

Response:
HTTP 204 No Content           

複制

ROA 風格和 restful 規範說明的是一回事,為友善其與 RPC 風格接口的對比,特此說明上面示例的一些值得關注的點:

  • 使用 HTTP 響應碼(200,201,204),完成 HTTP 語義與業務語義的映射,異常流也出現 404,401 等情況(出于篇幅考慮,本文未做異常流的介紹)
  • PATCH 部分修改資源,請求體是修改部分的内容;PUT 建立/修改資源,請求體是新資源全部的内容
  • id 是資源定位符,而 age、name 則為屬性

RPC 風格

建立使用者(POST)

Request:
POST /user/createUser

{"name": "kirito", "age": 18}

Response:
HTTP 200 OK

{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 18}}           

複制

查詢使用者(POST)

Request:
POST /user/getUser

{"id": 1}

Response:
HTTP 200 OK

{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 18}}           

複制

查詢使用者清單(POST)

Request:
POST /user/listUsers

Response:
HTTP 200 OK

{"code": 0, "message": "", "data": {"user": [{"id": 1, "name": "kirito", "age": 18}], "next": "/user/listUsers?offset=1"}}           

複制

修改使用者(POST)

Request:
POST /user/modifyUser

{"id": 1, "name": "kirito", "age": 19}

Response:
HTTP 200 OK

{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 19}}           

複制

修改使用者名稱(POST)

Request:
POST /user/modifyUserAge

{"id": 1, "age": 20}

Response:
HTTP 200 OK

{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 20}}           

複制

删除使用者(DELETE)

Request:
POST /user/deleteUser

{"id": 1}

Response:
{"code": 0, "message": ""}           

複制

RPC 風格不像 restful 一類的 ROA 風格存在一些約定俗成的規範,每個業務系統在落地時,都存在差異,故此處隻是筆者個人的經驗之談,但願讀者能夠求同存異:

  • user 為子產品名,不需要像 ROA 風格使用複數形式
  • 使用明确的動賓結構,而不是将 CRUD 映射到 HTTP Method,HTTP Method 統一使用 POST,查詢場景也可以使用 GET
  • 傳回值中攜帶 code、message 和 data,來映射響應狀态及響應資訊,一般可以自行定義 code 的狀态碼,本文使用 0 辨別請求成功,message 僅在業務響應失敗時有意義,data 代表業務響應結果

如何選擇 RPC 和 ROA,則需要根據産品自身的業務情況進行決策。有如下的指導原則:

  • 有複雜業務邏輯的 API ,無法使用簡單的增、删、改、查描述時宜使用 RPC 風格。
  • 如果業務所屬行業标準要求 restful 風格 API 或 ROA 能夠滿足業務需求,宜使用 ROA 風格。

AWS 主要采用 RPC 風格,Azure、Google 主要采用 ROA(restful)風格,阿裡雲 OpenAPI 同時支援 RPC 和 ROA,以 RPC 為主。

盡管規範是無罪的,但在 ROA 風格在實踐過程中,我還是見識過不少“坑”的:

  • 要求資源先行,即先設計資源,後設計接口,對軟體開發流程要求較高
  • 錯誤的 ROA 設計案例 1:tomcat 等應用伺服器在處理 DELETE 方法的 HTTP 請求時,預設不允許攜帶 request body,需要顯式開啟,導緻删除失敗。(此案例為設計者的問題,複雜的删除場景,不應當映射成 DELELE,而應改成 POST,DELETE 不應當攜帶 request body)
  • 錯誤的 ROA 設計案例 2:restful 路徑中攜帶的參數,可能會引發正則比對的問題,例如誤将郵箱作為路徑參數,或者多級路徑比對的沖突問題(此案例為設計者的問題,複雜的查詢場景,不應當映射成 GET,而應改成 POST,path 中隻應該出現資源定位符,而不應當攜帶屬性)
  • 響應碼為 404 時,較難區分是真的 path 不存在,還是資源不存在
  • 不利于對接網關等需要配置路由轉發的場景

CSB 的 Open API 規範希望滿足以下的需求:

  • 後端開發設計接口時,有明确的設計思路,不至于因為一個接口到底用 POST 還是 GET 實作而糾結,不用花費太多時間在資源的抽象上(這并不是說明資源是不需要被設計的)
  • 前端開發對接接口時,能夠較快地與後端協同,并且利于前端接口的封裝
  • 使用者對接 Open API 時,整體風格一緻,子產品清晰

綜上,在設計風格選擇上,我計劃采取 RPC 的設計規範。總結一下 RPC 風格的優勢:

  • API 設計難度較低,容易落地
  • 阿裡雲大多數成熟的 IAAS 層産品使用 RPC 規範
  • 适合複雜業務場景

一個詳細的 RPC 接口文檔示例

建立服務

請求參數

序号 字段中文名 字段英文名 資料類型 必填 說明
1 名稱 name string 顯示名稱
2 協定 protocol string 枚舉值:http/grpc/webservice
3 負載均衡 lb string 枚舉值:random/roundrobin
4 上遊類型 upstreamType string 枚舉值:fixed/discovery
5 節點清單 nodes array upstreamType=fixed 時必填,示例:[{"host": "1.1.1.1","port": "80","weight": "1"}]
6 來源id originId string
7 服務名稱 serviceName string 注冊中心中的名稱,upstreamType=discovery 時必填
8 服務描述 description string
9 網關id gatewayId string

傳回參數

序号 字段中文名 字段英文名 資料類型 說明
1 響應碼 code int 0 辨別成功;1 辨別失敗
2 響應資訊 message string
3 響應結果 data string 傳回服務 id

請求示例

POST /service/createService

Request:
{
  "name": "httpbin",
  "protocol": "http",
  "lb": "random",
  "upstreamType": "fixed",
  "nodes": [
    {
      "host": "httpbin.org",
      "port": "80",
      "weight": "1"
    }
  ],
  "gatewayId": "gw-1qw2e3e4"
}

Response:
{
  "code": 0,
  "message": "",
  "serviceId": "s-1qw2e3e4"
}           

複制

API 命名規範

  • API 應使用拼寫正确的英文,符合文法規範,包括單複數、時态和語言習慣
  • 不能出現多個含義相近但功能無實際差别的 API,如同時存在 /user/getUser 和 /user/describeUser
  • 語言習慣:禁止使用拼音
  • 如下常見場景的命名規則是固定的
    • 日期時間類型的參數應命名為 XxxxTime。例如:CreateTime
  • 常用操作名稱規範
    • create:建立
    • modify:變更
    • delete:删除
    • get:擷取單個資源詳情
    • list:擷取資源清單
    • establishRelation:建立資源關系
    • destroyRelation:銷毀資源關系

總結

以本文推崇的一條規範為例:"所有接口全部使用 POST",這不是為了遷就低水準不思進取的架構師和前後端程式員們(我在社群論壇上看到的言論),而是為了提高開發效率,降低溝通成本,降低運維和錯誤定位成本,把瞎折騰的成本,投入到了其他比如業務架構設計,測試體系,線上監控,容災降級等領域上。

接口規範也并非我總結的那樣,隻有 RPC 和 ROA,也有一些言論将 GraphQL 單獨歸為一類 API 設計風格,用于複雜查詢場景,有興趣的同學可以參考 es 的 API 文檔。

綜上,我計劃采用 RPC 的 API 設計風格。

參考資料

kong:https://docs.konghq.com/gateway/2.8.x/admin-api/

google restful api design:https://cloud.google.com/apis/design?hl=zh-cn

https://www.zhihu.com/question/336797348