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í)踐,也就是本文。

為什么我們需要 API 統(tǒng)一規(guī)范

這個(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ī)范不顧。

規(guī)范的原則

在制定 API 規(guī)范時(shí),我們應(yīng)該遵循一些基本原則,以應(yīng)對(duì)技術(shù)上的分歧,我總結(jié)了三個(gè)獲得廣泛認(rèn)可的原則:

principle

image by alswl

REST 到底行不行?

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

  1. 1. The Swamp of POX,使用 HTTP 承載 Legacy 協(xié)議(XML)
  2. 2. Resources:使用資源抽象
  3. 3. HTTP Verbs:使用豐富的 HTTP Verbs
  4. 4.?Hypermedia Controls:使用?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 也有一些局限性和缺陷:

因此,雖然 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é)。

Web API 規(guī)范的選擇題

因?yàn)槲覀兊膮f(xié)議基于 HTTP 和 REST 設(shè)計(jì),我們將以 HTTP 請(qǐng)求的四個(gè)核心部分為基礎(chǔ)展 開討論,這些部分分別是:URL、Header、Request 和 Response。

URL 最佳實(shí)踐

我的 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 解釋自增 IDGUIDReadable 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 URLAlias URL 解釋???
URL 中模型是否允許縮寫(或精簡)模型縮寫解釋???
URL 中模型多個(gè)詞語拼接的連字符-_Camel
是否要區(qū)分 Web API 以及 Open API(面向非瀏覽器)???

版本在 URL 的優(yōu)勢(shì)

我們?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。

HTTP Verb 不夠用的情況

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ì)這種情況,有兩種可行的選擇:

  1. 1. 使用 POST 方法與自定義動(dòng)詞,例如 POST /apis/books/borrow,表示借書這一動(dòng)作;
  2. 2. 創(chuàng)建一個(gè)借書記錄,使用資源新增方式來結(jié)構(gòu)不存在的動(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)。

Readable Stable Identity 解釋

在標(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 的特性。

一級(jí)和多級(jí)的解釋

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(基于類別)。

Alias URL 解釋

對(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è)和工作。

Header 最佳實(shí)踐

我們常常會(huì)忽略 Header 的重要性。實(shí)際上,HTTP 動(dòng)詞的選擇、HTTP 狀態(tài)碼以及各種身 份驗(yàn)證邏輯(例如 Cookie / Basic Auth / Berear Token)都依賴于 Header 的設(shè)計(jì)。

設(shè)計(jì)風(fēng)格選擇

問題解釋(見下方單列分析)方案 A方案 B方案 C
是否所有 Verb 都使用 POST關(guān)于全盤 POST???
修改(Modify)動(dòng)作是 POST 還是 PATCH?POSTPATCH
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???

關(guān)于全盤 POST

有些新手(或者自認(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ù)雜還是更為簡潔?

Request 最佳實(shí)踐

設(shè)計(jì)風(fēng)格選擇

問題解釋(見下方單列分析)方案 A方案 B方案 C
復(fù)雜的參數(shù)是放到 Form Fields 還是單獨(dú)一個(gè) JSON BodyForm FieldsBody
子資源是一次性查詢還是獨(dú)立查詢嵌套獨(dú)立查詢
分頁參數(shù)存放HeaderURL Query
分頁方式分頁方式解釋Page basedOffset basedContinuation 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)致缺頁或者白屏。

Response 最佳實(shí)踐

設(shè)計(jì)風(fēng)格選擇

問題解釋(見下方單列分析)方案 A方案 B方案 C
模型呈現(xiàn)種類模型的幾種形式單一模型多種模型
大模型如何包含子模型模型模型的連接、側(cè)載和嵌入嵌入核心模型 + 多次關(guān)聯(lián)資源查詢鏈接
字段返回是按需還是歸并還是統(tǒng)一統(tǒng)一使用 fields 字段按需
字段表現(xiàn)格式SnakeCamel
錯(cuò)誤碼無自定,使用 Message自定義
錯(cuò)誤格式全局統(tǒng)一按需
時(shí)區(qū)UTCLocalLocal + 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ì)的。

一張風(fēng)格 Checklist

Github 風(fēng)格

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 ModifyResources in the REST API#client-errors[21]
是否校驗(yàn) UserAgent?
是否校驗(yàn) Referrral??
Request
復(fù)雜的參數(shù)是放到 Form Fields 還是單獨(dú)一個(gè) JSON BodyBody參考 Pulls#create-a-pull-request[22]
子資源是一次性查詢還是獨(dú)立查詢嵌套從 Pulls 進(jìn)行判斷
分頁參數(shù)存放URL Query
分頁方式PageUsing 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 風(fēng)格

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 IdentityGuidelines.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è)詞語拼接的連字符CamelJob Runs – List – REST API (Azure Storage Mover)[38]
是否要區(qū)分 Web API 以及 Open API(面向非瀏覽器)??
Header
是否所有 Verb 都使用 POST??
修改(Modify)動(dòng)作是 POST 還是 PATCH?PATCHAgents – Update – REST API (Azure Storage Mover)[39]
HTTP Status 返回值充分利用 HTTP StatusGuidelines.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 BodyBody參考 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)格

我個(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 BodyBody
子資源是一次性查詢還是獨(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 的一個(gè)有趣設(shè)計(jì)

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 不行

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)@窺豹

上一篇:

四種主流的API風(fēng)格介紹與對(duì)比

下一篇:

One-API實(shí)現(xiàn)大語言模型請(qǐng)求接口的統(tǒng)一
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)