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