
如何快速實現REST API集成以優化業務流程
pyramid
image by stable difussion, prompt by alswl
這個問題困擾了我很長時間,始于我求學時期,每一次都需要與團隊成員進行交流和討論。 從最初的自由風格到后來的 REST,我經常向項目組引用?Github v3[1]?和 Foursqure API(已經無法訪問,暴露年齡) 文檔。 然而,在實踐過程中,仍然會有一些與實際工作或公司通用規范不匹配的情況, 這時候我需要做一些補充工作。最終,我會撰寫一個簡要的?DEVELOPMENT.md
?文檔,以描述設計方案。
但我對該文檔一直有更多的想法,它還不夠完善。因此,我想整理出一份簡單(Simple)而實用(Pragmatic)的 Web API 最佳實踐,也就是本文。
這個問題似乎很明顯,但是深入剖析涉及團隊協作效率和工程設計哲學。
API(Application Programming Interface,應用程序編程接口)是不同軟件系統之間交互的橋梁。在不同軟件系統之間進行通信時, API 可以通過標準化的方式進行數據傳輸和處理,從而實現各種應用程序的集成。
當我們開始撰寫 API 文檔時,就會出現一個范式(Design Pattern),這是顯式還是隱式的, 是每個人一套還是公用同一套。這就像我們使用統一的 USB 接口一樣,統一降低了成本,避免了可能存在的錯誤。具體來說,這有以下幾個原因:
why
image by alswl
雖然使用統一規范確實有一些成本,需要框架性的了解和推廣,但我相信在大部分場景下, 統一規范所帶來的收益遠遠高于這些成本。
然而,并非所有的情況下都需要考慮 API 規范。對于一些短生命周期的項目、影響面非常小的內部項目和產品, 可能并不需要過多關注規范。 此外,在一些特殊的業務場景下, 協議底層可能會發生變化,這時候既有的規范可能不再適用。但即使如此,我仍然建議重新起草新的規范,而不是放棄規范不顧。
在制定 API 規范時,我們應該遵循一些基本原則,以應對技術上的分歧,我總結了三個獲得廣泛認可的原則:
principle
image by alswl
在 Web API 領域,RESTful API[2]?已經成為廣受歡迎的協議。 其廣泛適用性和受眾范圍之廣源于其與 HTTP 協議的綁定,這使得 RESTful API 能夠輕松地與現有的 Web 技術進行交互。如果您對 REST 不熟悉, 可以查看?阮一峰的 RESTful API 設計指南[3]?以及?RESTful API 設計最佳實踐[4]。
REST 是一種成熟度較高的協議,Leonard Richardson[5]?將其描述為四種成熟度級別:
rest-four-level
image by alswl
rel
?鏈接進行 API 資源整合,JSON:API[6]?是登峰造極的表現REST 的核心優勢在于:
然而,REST 并非一種具體的協議或規范,而是一種風格理念。盡管 REST 定義了一些規則和原則,如資源的標識、統一接口、無狀態通信等, 但它并沒有規定一種具體的實現方式。因此,在實際開發中,不同的團隊可能會有不同的理解和實踐, 從而導致 API 的不一致性和可維護性降低。
此外,REST 也有一些局限性和缺陷:
/login
)操作,轉換成 session
就非常繞口; 同樣的問題在轉賬這種業務也會出現。HTTP 有限的動詞無法支撐所有業務場景。因此,雖然 REST 風格是一個不錯的指導思想,但在具體實現時需要結合具體業務需求和技術特點,有所取舍,才能實現良好的 API 設計。 最后,我們是否需要 Web API 設計規范,遵循 REST 風格呢?我認為 REST 能夠解決 90% 的問題,但還有 10% 需要明確規定細節。
因為我們的協議基于 HTTP 和 REST 設計,我們將以 HTTP 請求的四個核心部分為基礎展 開討論,這些部分分別是:URL、Header、Request 和 Response。
我的 URL 設計啟蒙來自于?Ruby on Rails[7]。 在此之前,我總是本能地將模型信息放到 URL 之上,但實際上良好的 URL 設計應該是針對系統信息結構的規劃。 因此,URL 設計不僅僅要考慮 API,還要考慮面向用戶的 Web URL。
為了達到良好的 URL 設計,我總結了以下幾個規則:
通常情況下,URL 的模型如下所示:
/$(prefix)/$(module)/$(model)/$(sub-model)/$(verb)?$(query)#${fragment}
其中,Prefix 可能是 API 的版本,也可能是特殊限定,如有些公司會靠此進行接入層分流; Module 是業務模塊,也可以省略;Model 是模型;SubModel 是子模型,可以省略; Verb 是動詞,也可以省略;Query 是請求參數;Fragment 是 HTTP 原語 Fragment。
需要注意的是,并非所有的組成部分都是必須出現的。例如,SubModel 和 Verb 等字段可 以在不同的 URL 風格中被允許隱藏。
設計風格選擇
注:請注意,方案 A / B / C 之間沒有關聯,每行上下也沒有關聯
問題 | 解釋(見下方單列分析) | 方案 A | 方案 B | 方案 C |
API Path 里面 Prefix | /apis | /api | 二級域名 | |
Path 里面是否包含 API 版本 | 版本在 URL 的優勢 | ? | ?? | |
Path 是否包含 Group | ? | ?? | ||
Path 是否包含動作 | HTTP Verb 不夠用的情況 | ? | ?? (純 REST) | 看情況(如果 HTTP Verb CRUD 無法滿足就包含) |
模型 ID 形式 | Readable Stable Identity 解釋 | 自增 ID | GUID | Readable Stable ID |
URL 中模型單數還是復數 | 單數 | 復數 | 列表復數,單向單數 | |
資源是一級(平鋪)還是多級(嵌套) | 一級和多級的解釋 | 一級(平鋪) | 多級(嵌套) | |
搜索如何實現,獨立接口(/models/search )還是基于列表/models/ 接口 | 獨立 | 合并 | ||
是否有 Alias URL | Alias URL 解釋 | ? | ?? | |
URL 中模型是否允許縮寫(或精簡) | 模型縮寫解釋 | ? | ?? | |
URL 中模型多個詞語拼接的連字符 | - | _ | Camel | |
是否要區分 Web API 以及 Open API(面向非瀏覽器) | ? | ?? |
我們在設計 URL 時遵循一致性的原則,無論是哪種身份或狀態,都會使用相同的 URL 來訪問同一個資源。 這也是 Uniform Resource Location 的基本原則。雖然我們可以接受不同的內容格式(例如 JSON / YAML / HTML / PDF / etc), 但是我們希望資源的位置是唯一的。
然而,問題是,對于同一資源在不同版本之間的呈現,是否應該在 URL 中體現呢?這取決于設計者是否認為版本化屬于位置信息的范疇。
根據 RFC 的設計,除了 URL 還有 URN(Uniform Resource Name)[8], 后者是用來標識資源的,而 URL 則指向資源地址。實際上,URN 沒有得到廣泛的使用,以至于 URI 幾乎等同于 URL。
在 REST 設計中,我們需要使用 HTTP 的 GET / POST / PUT / DELETE / PATCH / HEAD 等動詞對資源進行操作。 比如使用 API?GET /apis/books
?查看書籍列別,這個自然且合理。 但是,當需要執行類似「借一本書」這樣的動作時, 我們沒有合適的動詞(BORROW)來表示。針對這種情況,有兩種可行的選擇:
POST /apis/books/borrow
,表示借書這一動作;POST /apis/books/borrow-log/
;這個問題在復雜的場景中會經常出現,例如用戶登錄(POST /api/auth/login
vs POST /api/session
)和帳戶轉賬(vs 轉賬記錄創建)等等。 API 抽象還是具體,始終離不開業務的解釋。我們不能簡單地將所有業務都籠統概括到 CRUD 上面, 而是需要合理劃分業務,以便更清晰地實現和讓用戶理解。
在進行設計時,我們可以考慮是否需要為每個 API 創建一個對應的按鈕來方便用戶的操作。 如果系統中只有一個名為?/api/do
?的 API 并將所有業務都綁定在其中,雖然技術上可行, 但這種設計不符合業務需求,每一層的抽象都是為了標準化解決特定問題的解法,TCP L7 設計就是這種理念的體現。
在標記一個資源時,我們通常有幾種選擇:
我個人有一個設計小技巧:使用?${type}/${type-id}
?形式的 slug 來描述標識符。Slug 是一種人類可讀的唯一標識符, 例如?hostname/abc.sqa
?或?ip/172.133.2.1
。 這種設計方式可以在可讀性和唯一性之間實現很好的平衡。
A slug is a human-readable, unique identifier, used to identify a resource instead of a less human-readable identifier like an id .
from What’s a slug. and why would I use one? | by Dave Sag[9]
PS:文章最末我還會介紹一套 Apple Music 方案,這個方案兼顧了 ID / Readable / Stable 的特性。
URL 的層級設計可以根據建模來進行,也可以采用直接單層結構的設計。具體問題的解決方式, 例如在設計用戶擁有的書籍時,可以選擇多級結構的 /api/users/foo/books
或一級結構的 /api/books?owner=foo
。
技術上這兩種方案都可以,前者尊重模型的歸屬關系,后者則是注重 URL 結構的簡單。
多級結構更直觀,但也需要解決可能存在的多種組織方式的問題,例如圖書館中書籍按照作者或類別進行組織? 這種情況下,可以考慮在多級結構中明確模型的歸屬關系, 例如 /api/author/foo/books
(基于作者)或 /api/category/computer/books
(基于類別)。
對于一些頻繁使用的 URL,雖然可以按照 URL 規則進行設計,但我們仍然可以設計出一個更為簡潔的 URL, 以方便用戶的展示和使用。這種設計在 Web URL 中尤其常見。比如一個圖書館最熱門書籍的 API:
# 原始 URL
https://test.com/apis/v3/books?sort=hot&limit=10
# Alias URL
https://test.com/apis/v3/books/hot
通常,在對資源進行建模時,會使用較長的名稱來命名,例如書籍索引可能被命名為?BookIndex
?,而不是?Index
。 在 URL 中呈現時,由于?/book/book-index
?的 URL 前綴包含了 Book,我們可以減少一層描述, 使 URL 更為簡潔,例如使用?/book/index
。這種技巧在 Web URL 設計中非常常見。
此外,還有一種模型縮寫的策略,即提供一套完整的別名注冊方案。別名是全局唯一的, 例如在 Kubernetes 中,?Deployment[10]?是一種常見的命名,而?apps/v1/Deployment
?是通過添加 Group 限定來表示完整的名稱, 同時還有一個簡寫為?deploy
。這個機制依賴于 Kubernetes 的 API Schema 系統進行注冊和工作。
我們常常會忽略 Header 的重要性。實際上,HTTP 動詞的選擇、HTTP 狀態碼以及各種身 份驗證邏輯(例如 Cookie / Basic Auth / Berear Token)都依賴于 Header 的設計。
問題 | 解釋(見下方單列分析) | 方案 A | 方案 B | 方案 C |
是否所有 Verb 都使用 POST | 關于全盤 POST | ? | ?? | |
修改(Modify)動作是 POST 還是 PATCH? | POST | PATCH | ||
HTTP Status 返回值 | 2XX 家族 | 充分利用 HTTP Status | 只用核心狀態(200 404 302 等) | 只用 200 |
是否使用考慮限流系統 | ? 429 | ?? | ||
是否使用緩存系統 | ? ETag / Last Modify | ?? | ||
是否校驗 UserAgent | ? | ?? | ||
是否校驗 Referrral | ? | ?? |
有些新手(或者自認為有經驗的人)可能得出一個錯誤的結論,即除了 GET 請求以外, 所有的 HTTP 請求都應該使用 POST 方法。甚至有些人要求 所有行為(即使是只讀的請求)也應該使用 POST 方法[11]。 這種觀點通常會以“簡單一致”、“避免緩存”或者“運營商的要求”為由來支持。
然而,我們必須明白 HTTP 方法的設計初衷:它是用來描述資源操作類型的,從而派生出了包括緩存、安全、冪等性等一系列問題。 在相對簡單的場景下,省略掉這一層抽象的確不會帶來太大的問題,但一旦進入到復雜的領域中, 使用 HTTP 方法這一層抽象就顯得非常重要了。這是否遵循標準將決定你是否能夠獲得標準化帶來的好處, 類比一下就像一個新的手機廠商可以選擇不使用 USB TypeC 接口。 技術上來說是可行的,但同時也失去了很多標準化支持和大家心智上的約定俗成。
我特別喜歡一位 知乎網友[12] 的 評論[13]:「路由沒有消失,只是轉移了」。
2XX 家族
HTTP 狀態碼的用途在于表明客戶端與服務器間通信的結果。2XX 狀態碼系列代表服務器已經成功接收、 理解并處理了客戶端請求,回應的內容是成功的。以下是 2XX 系列中常見的狀態碼及其含義:
2XX 系列的狀態碼表示請求已被成功處理,這些狀態碼可以讓客戶端明確知曉請求已被正確處理,從而進行下一步操作。
是否需要全面使用 2XX 系列的狀態碼,取決于是否需要向客戶端明確/顯示的信息, 告知它下一步動作。如果已經通過其他方式(包括文檔、口頭協議)描述清楚, 那么確實可以通盤使用 200 狀態碼進行返回。但基于行為傳遞含義, 或是基于文檔(甚至口頭協議)傳遞含義,哪種更優秀呢?是更為復雜還是更為簡潔?
設計風格選擇
問題 | 解釋(見下方單列分析) | 方案 A | 方案 B | 方案 C |
復雜的參數是放到 Form Fields 還是單獨一個 JSON Body | Form Fields | Body | ||
子資源是一次性查詢還是獨立查詢 | 嵌套 | 獨立查詢 | ||
分頁參數存放 | Header | URL Query | ||
分頁方式 | 分頁方式解釋 | Page based | Offset based | Continuation token |
分頁控制者 | 分頁控制著解釋 | 客戶端 | 服務端 |
分頁方式解釋
我們最為常見的兩種分頁方式是 Page-based 和 Offset-based,可以通過公式進行映射。 此外,還存在一種稱為 Continuation Token 的方式,其技術類似于 Oracle 的 rownum 分頁方案[14],使用參數 start-from=?
進行描述。 雖然 Continuation Token 的優缺點都十分突出,使用此種方式可以將順序性用于替代隨機性。
分頁控制著解釋
在某些情況下,我們需要區分客戶端分頁(Client Pagination)和服務器分頁(Server Pagniation)。 客戶端分頁是指下一頁的參數由客戶端計算而來,而服務器分頁則是由服務器返回?rel
?或 JSON.API 等協議。 使用服務器分頁可以避免一些問題,例如批量屏蔽了一些內容,如果使用客戶端分頁,可能會導致缺頁或者白屏。
設計風格選擇
問題 | 解釋(見下方單列分析) | 方案 A | 方案 B | 方案 C |
模型呈現種類 | 模型的幾種形式 | 單一模型 | 多種模型 | |
大模型如何包含子模型模型 | 模型的連接、側載和嵌入 | 嵌入 | 核心模型 + 多次關聯資源查詢 | 鏈接 |
字段返回是按需還是歸并還是統一 | 統一 | 使用 fields 字段按需 | ||
字段表現格式 | Snake | Camel | ||
錯誤碼 | 無自定,使用 Message | 自定義 | ||
錯誤格式 | 全局統一 | 按需 | ||
時區 | UTC | Local | Local + TZ | |
HATEOAS | ? | ?? |
模型的幾種形式
在 API 設計中,對于模型的表現形式有多種定義。雖然這并不是 API 規范必須討論的話題,但它對于 API 設計來說是非常重要的。
我將模型常說的模型呈現方式分為一下幾類,這并非是專業的界定,借用了 Java 語境下面的一些定義。 這些名稱在不同公司甚至不同團隊會有不一樣的叫法:
models
image by alswl
除此之外,還經常使用兩類:Rich Model 和 Tiny Model(請忽略命名,不同團隊叫法差異比較大):
模型的連接、側載和嵌入
在 API 設計中,我們經常需要處理一個模型中包含多個子模型的情況,例如 Book 包含 Comments。 對于這種情況,通常有三種表現形式可供選擇:鏈接(Link)、側載(Side)和嵌入(Embed)。
models-with-children
image by alswl
鏈接(有時候這個 URL 也會隱藏,基于客戶端和服務端的隱式協議進行請求):
{
"data": {
"id": 42,
"name": "朝花夕拾",
"relationships": {
"comments": "http://www.domain.com/book/42/comments",
"author": [
"http://www.domain.com/author/魯迅"
]
}
}
}
側載:
{
"data": {
"id": 42,
"name": "朝花夕拾",
"relationships": {
"comments": "http://www.domain.com/book/42/comments",
"authors": [
"http://www.domain.com/author/魯迅"
]
}
},
"includes": {
"comments": [
{
"id": 91,
"author": "匿名",
"content": "非常棒"
}
],
"authors": [
{
"name": "魯迅",
"description": "魯迅原名周樹人"
}
]
}
}
嵌入:
{
"data": {
"id": 42,
"name": "朝花夕拾",
"comments": [
{
"id": 91,
"author": "匿名",
"content": "非常棒"
}
],
"authors": [
{
"name": "魯迅",
"description": "魯迅原名周樹人"
}
]
}
}
還有一些問題沒有收斂在四要素里面,但是我們在工程實踐中也經常遇到,我將其捋出來:
我不是 HTTP 協議,怎么辦?
Web API 中較少遇到非 HTTP 協議,新建一套協議的成本太高了。在某些特定領域會引入一些協議, 比如 IoT 領域的?MQTT[15]。
此外,RPC 是一個涉及廣泛領域的概念,其內容遠遠不止于協議層面。 通常我們會將 HTTP 和 RPC 的傳輸協議以及序列化協議進行對比。 我認為,本文中的許多討論也對 RPC 領域具有重要意義。
有些團隊或個人計劃使用自己創建的協議,但我的觀點是應盡量避免自建協議,因為真正需要創建協議的情況非常罕見。 如果確實存在強烈的需要,那么我會問兩個問題:是否通讀過 HTTP RFC 文檔和 HTTP/2 RFC 文檔?
我不是遠程服務(RPC / HTTP 等),而是 SDK 怎么辦?
本文主要討論的是 Web API(HTTP)的設計規范,并且其中一些規則可以借鑒到 RPC 系統中。 然而,討論的基礎都是建立在遠程服務(Remote Service)的基礎之上的。 如果你是 SDK 開發人員,你會有兩個角色,可能會作為客戶端和遠程服務器進行通信, 同時還會作為 SDK 提供面向開發人員的接口。對于后者,以下幾個規范可以作為參考:
后者可以參考一下這么幾個規范:
認證鑒權方案
一般而言,Web API 設計中會明確描述所采用的認證和鑒權系統。 需要注意區分「認證」和「鑒權」兩個概念。關于「認證」這一話題,可以在單獨的章節中進行討論,因此本文不會展開這一方面的內容。
在 Web API 設計中,常見的認證方式包括:HTTP Basic Auth、OAuth2 和賬號密碼登錄等。 常用的狀態管理方式則有 Bearer Token 和 Cookie。此外,在防篡改等方面,還會采用基于 HMac 算法的防重放和篡改方案。
忽略掉的話題
在本次討論中,我未涉及以下話題:異步協議(Web Socket / Long Pulling / 輪訓)、CORS、以及安全問題。 雖然這些話題重要,但是在本文中不予展開。
什么時候打破規則
有些開發者認為規則就是為了打破而存在的?,F實往往非常復雜,我們難以討論清楚各個細節。 如果開發者覺得規則不符合實際需求,有兩種處理方式:修改規則或打破規則。 然而,我更傾向于討論和更新規則,明確規范不足之處,確定是否存在特殊情況。 如果確實需要創建特例,一定要在文檔中詳細描述,告知接任者和消費者這是一個特例,說明特例產生的原因以及特例是如何應對的。
Github 的 API 是我常常參考的對象。它對其業務領域建模非常清晰,提供了詳盡的文檔,使得溝通成本大大降低。 我主要參考以下兩個鏈接: API 定義?GitHub REST API documentation[18]?和 面向應用程序提供的 API 列表?Endpoints available for GitHub Apps[19]?,該列表幾乎包含了 Github 的全部 API。
問題 | 選擇 | 備注 |
URL | ||
API Path 里面 Prefix | 二級域名 | https://api.github.com |
Path 里面是否包含 API 版本 | ?? | Header X-GitHub-Api-Version API Versions[20] |
Path 是否包含 Group | ?? | |
Path 是否包含動作 | 看情況(如果 HTTP Verb CRUD 無法滿足就包含) | 比如 PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge POST /repos/{owner}/{repo}/releases/generate-notes |
模型 ID 形式 | Readable Stable Identity | |
URL 中模型單數還是復數 | 復數 | |
資源是一級(平鋪)還是多級(嵌套) | 多級 | |
搜索如何實現,獨立接口(/models/search )還是基于列表/models/ 接口 | 獨立 | |
是否有 Alias URL | ? | |
URL 中模型是否允許縮寫(或精簡) | ?? | 沒有看到明顯信息,基于多級模型也不需要,但是存在 GET /orgs/{org}/actions/required_workflows |
URL 中模型多個詞語拼接的連字符 | - 和 _ | GET /repos/{owner}/{repo}/git/matching-refs/{ref} vs GET /orgs/{org}/actions/required_workflows |
是否要區分 Web API 以及 Open API(面向非瀏覽器) | ?? | |
Header | ||
是否所有 Verb 都使用 POST | ?? | |
修改(Modify)動作是 POST 還是 PATCH? | PATCH | |
HTTP Status 返回值 | 充分利用 HTTP Status | 常用,包括限流洗損 |
是否使用考慮限流系統 | ? 429 | |
是否使用緩存系統 | ? ETag / Last Modify | Resources in the REST API#client-errors[21] |
是否校驗 UserAgent | ? | |
是否校驗 Referrral | ?? | |
Request | ||
復雜的參數是放到 Form Fields 還是單獨一個 JSON Body | Body | 參考 Pulls#create-a-pull-request[22] |
子資源是一次性查詢還是獨立查詢 | 嵌套 | 從 Pulls 進行判斷 |
分頁參數存放 | URL Query | |
分頁方式 | Page | Using pagination in the REST API[23] |
分頁控制者 | 服務端 | 同上 |
Response | ||
模型呈現種類 | 多種模型 | 比如 Commits 里面的 明細和 Parent Commits[24] |
大模型如何包含子模型模型 | 核心模型 + 多次關聯資源查詢? | 沒有明確說明,根據幾個核心 API 反推 |
字段返回是按需還是歸并還是統一 | 統一 | |
字段表現格式 | Snake | |
錯誤碼 | 無 | Resources in the REST API#client-errors[25] |
錯誤格式 | 全局統一 | Resources in the REST API#client-errors[26] |
時區 | 復合方案(ISO 8601 > Time-Zone Header > User Last > UTC) | Resources in the REST API#Timezones[27] |
HATEOAS | ?? |
Azure 的 API 設計遵循?api-guidelines/Guidelines.md at master · microsoft/api-guidelines[28], 這篇文章偏原理性,另外還有一份實用指導手冊在?Best practices in cloud applications[29]?和?Web API design best practices[30]。
需要注意的是,Azure 的產品線遠比 Github 豐富,一些 API 也沒有遵循 Azure 自己的規范。 在找實例時候,我主要參考?REST API Browser[31],?Azure Storage REST API Reference[32]。 如果具體實現和 Guidelines.md 沖突,我會采用 Guidelines.md 結論。
問題 | 選擇 | 備注 |
URL | ||
API Path 里面 Prefix | 二級域名 | |
Path 里面是否包含 API 版本 | ?? | x-ms-version |
Path 是否包含 Group | ? | |
Path 是否包含動作 | ??? | 沒有明確說明,但是有傾向使用 comp 參數來進行動作,保持 URL 的 RESTful 參考 Lease Container (REST API) – Azure Storage[33] |
模型 ID 形式 | Readable Stable Identity | Guidelines.md#73-canonical-identifier[34] |
URL 中模型單數還是復數 | 復數 | Guidelines.md#93-collection-url-patterns[35] |
資源是一級(平鋪)還是多級(嵌套) | 多級 / 一級 | api-design#define-api-operations-in-terms-of-http-methods[36],注 MS 有 comp=? 這種參數,用來處理特別的命令 |
搜索如何實現,獨立接口(/models/search )還是基于列表/models/ 接口 | ? | 傾向于基于列表,因為大量使用 comp= 這個 URL Param 來進行子命令,比如 Incremental Copy Blob (REST API) – Azure Storage[37] |
是否有 Alias URL | ? | |
URL 中模型是否允許縮寫(或精簡) | ? | |
URL 中模型多個詞語拼接的連字符 | Camel | Job Runs – List – REST API (Azure Storage Mover)[38] |
是否要區分 Web API 以及 Open API(面向非瀏覽器) | ?? | |
Header | ||
是否所有 Verb 都使用 POST | ?? | |
修改(Modify)動作是 POST 還是 PATCH? | PATCH | Agents – Update – REST API (Azure Storage Mover)[39] |
HTTP Status 返回值 | 充分利用 HTTP Status | Guidelines.md#711-http-status-codes[40] |
是否使用考慮限流系統 | ? | |
是否使用緩存系統 | ? | Guidelines.md#75-standard-request-headers[41] |
是否校驗 UserAgent | ?? | |
是否校驗 Referrral | ?? | |
Request | ||
復雜的參數是放到 Form Fields 還是單獨一個 JSON Body | Body | 參考 Agents – Create Or Update – REST API (Azure Storage Mover)[42] |
子資源是一次性查詢還是獨立查詢 | ? | |
分頁參數存放 | ? | 沒有結論 |
分頁方式 | Page based | |
分頁控制者 | 服務端 | Agents – List – REST API (Azure Storage Mover)[43] |
Response | ||
模型呈現種類 | 單一模型 | 推測 |
大模型如何包含子模型模型 | ? | 場景過于復雜,沒有單一結論 |
字段返回是按需還是歸并還是統一 | ? | |
字段表現格式 | Camel | |
錯誤碼 | 使用自定錯誤碼清單 | 至少在各自產品內 |
錯誤格式 | 自定義 | |
時區 | ? | |
HATEOAS | ? | api-design#use-hateoas-to-enable-navigation-to-related-resources[44] |
Azure 的整體設計風格要比 Github API 更復雜,同一個產品的也有多個版本的差異,看 上去統一性要更差一些。這種復雜場景想用單一的規范約束所有團隊的確也是更困難的。 我們可以看到 Azaure 團隊在 Guidelines 上面努力,他們最近正在推出 vNext 規范。
我個人風格基本繼承自 Github API 風格,做了一些微調,更適合中小型產品開發。 我的改動原因都在備注中解釋,改動出發點是:簡化 / 減少歧義 / 考慮實際成本。如果備注里面標記了「注」,則是遵循 Github 方案并添加一些觀點。
問題 | 選擇 | 備注 |
URL | ||
API Path 里面 Prefix | /apis | 我們往往只有一個系統,一個域名要承載 API 和 Web Page |
Path 里面是否包含 API 版本 | ? | |
Path 是否包含 Group | ? | 做一層業務模塊拆分,隔離一定合作邊界 |
Path 是否包含動作 | 看情況(如果 HTTP Verb CRUD 無法滿足就包含) | |
模型 ID 形式 | Readable Stable Identity | |
URL 中模型單數還是復數 | 復數 | |
資源是一級(平鋪)還是多級(嵌套) | 多級 + 一級 | 注:80% 情況都是遵循模型的歸屬,少量情況(常見在搜索)使用一級 |
搜索如何實現,獨立接口(/models/search )還是基于列表/models/ 接口 | 統一 > 獨立 | 低成本實現一些(早期 Github Issue 也是沒有 /search 接口 |
是否有 Alias URL | ?? | 簡單點 |
URL 中模型是否允許縮寫(或精簡) | ? | 一旦做了精簡,需要在術語表標記出來 |
URL 中模型多個詞語拼接的連字符 | - | |
是否要區分 Web API 以及 Open API(面向非瀏覽器) | ?? | |
Header | ||
是否所有 Verb 都使用 POST | ?? | |
修改(Modify)動作是 POST 還是 PATCH? | PATCH | |
HTTP Status 返回值 | 充分利用 HTTP Status | |
是否使用考慮限流系統 | ? 429 | |
是否使用緩存系統 | ?? | 簡單一些,使用動態數據,去除緩存能力 |
是否校驗 UserAgent | ? | |
是否校驗 Referrral | ?? | |
Request | ||
復雜的參數是放到 Form Fields 還是單獨一個 JSON Body | Body | |
子資源是一次性查詢還是獨立查詢 | 嵌套 | |
分頁參數存放 | URL Query | |
分頁方式 | Page | |
分頁控制者 | 客戶端 | 降低服務端成本,容忍極端情況空白 |
Response | ||
模型呈現種類 | 多種模型 | 使用的 BO / VO / Tiny / Rich |
大模型如何包含子模型模型 | 核心模型 + 多次關聯資源查詢 | |
字段返回是按需還是歸并還是統一 | 統一 | Tiny Model(可選) / Model(默認) / Rich Model(可選) |
字段表現格式 | Snake | |
錯誤碼 | 無 | 注:很多場景只要 message |
錯誤格式 | 全局統一 | |
時區 | ISO 8601 | 只使用一種格式,不再支持多種方案 |
HATEOAS | ?? |
Apple Music
image from Apple Music
我最近在使用 Apple Music 時注意到了其 Web 頁面的 URL 結構:
/cn/album/we-sing-we-dance-we-steal-things/277635758?l=en
仔細看這個 URL 結構,可以發現其中 Path 包含了人類可讀的 slug,分為三個部分:alumn/$(name)/$(id)
(其中包含了 ID)。 我立即想到了一個問題:中間的可讀名稱是否無機器意義,純粹面向自然人? 于是我測試了一個捏造的地址:/cn/album/foobar/277635758?l=en
。 在您嘗試訪問之前,您能猜出結果是否可以訪問嗎?
這種設計范式比我現在常用的 URL 設計規范要復雜一些。我的規范要求將資源定位使用兩層 slug 組織,即 $(type)/$(id)
。 而蘋果使用了 $(type)/(type-id)/$(id)
,同時照顧了可讀性和準確性。
GraphQL[45]?是一種通過使用自定義查詢語言來請求 API 的方式,它的優點在于可以提供更靈活的數據獲取方式。 相比于 RESTful API 需要一次請求獲取所有需要的數據,GraphQL 允許客戶端明確指定需要的數據,從而減少不必要的數據傳輸和處理。
然而,GraphQL 的過于靈活也是它的缺點之一。由于它沒有像 REST API 那樣有一些業務場景建模的規范, 開發人員需要自己考慮數據的處理方式。 這可能導致一些不合理的查詢請求,對后端數據庫造成過度的壓力。此外,GraphQL 的實現和文檔相對較少,也需要更多的學習成本。
因此,雖然 GraphQL 可以在一些特定的場景下提供更好的效果,但它并不適合所有的 API 設計需求。 實際上,一些公司甚至選擇放棄支持 GraphQL,例如 Github 的?一些項目[46]。
Complexity is incremental (復雜度是遞增的) – John Ousterhout (via[47])
風格沒有最好,只有最適合,但是擁有風格是很重要的。
建立一個優秀的規則不僅需要對現有機制有深刻的理解,還需要對業務領域有全面的掌握,并在團隊內進行有效的協作與溝通, 推廣并實施規則。 不過,一旦規則建立起來,就能夠有效降低系統的復雜度,避免隨著時間和業務的推進而不斷增加的復雜性, 并減少研發方面的溝通成本。
這是一項長期的投資,但能夠獲得持久的回報。希望有長遠眼光的人能夠注意到這篇文章。
主要參考文檔:
[1]
?Github v3:?https://docs.github.com/en/rest?apiVersion=2022-11-28[2]
?RESTful API:?https://en.wikipedia.org/wiki/Representational_state_transfer[3]
?阮一峰的 RESTful API 設計指南:?https://www.ruanyifeng.com/blog/2014/05/restful_api.html[4]
?RESTful API 設計最佳實踐:?https://www.oschina.net/translate/best-practices-for-a-pragmatic-restful-api?print[5]
?Leonard Richardson:?https://martinfowler.com/articles/richardsonMaturityModel.html#level0[6]
?JSON:API:?https://jsonapi.org/[7]
?Ruby on Rails:?https://guides.rubyonrails.org/routing.html[8]
?URN(Uniform Resource Name):?https://en.wikipedia.org/wiki/Uniform_Resource_Name[9]
?What’s a slug. and why would I use one? | by Dave Sag:?https://itnext.io/whats-a-slug-f7e74b6c23e0[10]
?Deployment:?https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#deployment-v1-apps[11]
?所有行為(即使是只讀的請求)也應該使用 POST 方法:?https://www.zhihu.com/question/336797348[12]
?知乎網友:?https://www.zhihu.com/people/huixiong-19[13]
?評論:?https://www.zhihu.com/question/336797348/answer/2198634068[14]
?rownum 分頁方案:?https://stackoverflow.com/questions/241622/paging-with-oracle[15]
?MQTT:?https://mqtt.org/[16]
?General Guidelines: API Design | Azure SDKs:?https://azure.github.io/azure-sdk/general_design.html[17]
?Low-Level I/O (The GNU C Library):?https://www.gnu.org/software/libc/manual/html_node/Low_002dLevel-I_002fO.html[18]
?GitHub REST API documentation:?https://docs.github.com/en/rest?apiVersion=2022-11-28[19]
?Endpoints available for GitHub Apps:?https://docs.github.com/en/rest/overview/endpoints-available-for-github-apps?apiVersion=2022-11-28[20]
?API Versions:?https://docs.github.com/en/rest/overview/api-versions?apiVersion=2022-11-28[21]
?Resources in the REST API#client-errors:?https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#client-errors[22]
?Pulls#create-a-pull-request:?https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#create-a-pull-request[23]
?Using pagination in the REST API:?https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api?apiVersion=2022-11-28[24]
?Commits:?https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28[25]
?Resources in the REST API#client-errors:?https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#client-errors[26]
?Resources in the REST API#client-errors:?https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#client-errors[27]
?Resources in the REST API#Timezones:?https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#timezones[28]
?api-guidelines/Guidelines.md at master · microsoft/api-guidelines:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md[29]
?Best practices in cloud applications:?https://learn.microsoft.com/en-us/azure/architecture/best-practices/index-best-practices[30]
?Web API design best practices:?https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design[31]
?REST API Browser:?https://learn.microsoft.com/en-us/rest/api/?view=Azure[32]
?Azure Storage REST API Reference:?https://learn.microsoft.com/en-us/rest/api/storageservices/[33]
?Lease Container (REST API) – Azure Storage:?https://learn.microsoft.com/en-us/rest/api/storageservices/lease-container?tabs=azure-ad[34]
?Guidelines.md#73-canonical-identifier:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#73-canonical-identifier[35]
?Guidelines.md#93-collection-url-patterns:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#93-collection-url-patterns[36]
?api-design#define-api-operations-in-terms-of-http-methods:?https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design#define-api-operations-in-terms-of-http-methods[37]
?Incremental Copy Blob (REST API) – Azure Storage:?https://learn.microsoft.com/en-us/rest/api/storageservices/incremental-copy-blob[38]
?Job Runs – List – REST API (Azure Storage Mover):?https://learn.microsoft.com/en-us/rest/api/storagemover/job-runs/list?tabs=HTTP[39]
?Agents – Update – REST API (Azure Storage Mover):?https://learn.microsoft.com/en-us/rest/api/storagemover/agents/update?tabs=HTTP[40]
?Guidelines.md#711-http-status-codes:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#711-http-status-codes[41]
?Guidelines.md#75-standard-request-headers:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md#75-standard-request-headers[42]
?Agents – Create Or Update – REST API (Azure Storage Mover):?https://learn.microsoft.com/en-us/rest/api/storagemover/agents/create-or-update?tabs=HTTP[43]
?Agents – List – REST API (Azure Storage Mover):?https://learn.microsoft.com/en-us/rest/api/storagemover/agents/list?tabs=HTTP[44]
?api-design#use-hateoas-to-enable-navigation-to-related-resources:?https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design#use-hateoas-to-enable-navigation-to-related-resources[45]
?GraphQL:?https://graphql.org/[46]
?一些項目:?https://github.blog/changelog/2022-08-18-deprecation-notice-graphql-for-packages/[47]
?via:?https://web.stanford.edu/~ouster/cgi-bin/cs190-winter18/lecture.php?topic=complexity[48]
?api-guidelines/Guidelines.md at master · microsoft/api-guidelines:?https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md[49]
?GitHub’s APIs:?https://docs.github.com/en/rest/overview/about-githubs-apis?apiVersion=2022-11-28[50]
?Web API design best practices – Azure Architecture Center | Microsoft Learn:?https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design[51]
?API 設計最佳實踐的思考 – 谷樸:?https://developer.aliyun.com/article/701810
本文章轉載微信公眾號@窺豹