系統設計 - RESTful API 使用問題和建議
雖然 RESTful API 已經成爲業界對於 API 的共識,但是不得不說,但是不得不說它具有很多侷限性。
RESTful API 和很多的技術流傳的原因類似:始於一種非常理想化的願景,但是在落地時卻需要做出權衡和取捨。
它的流行開始於 Roy Fielding 的演講,Roy Fielding 也是 HTTP 協議標準作者之一。
我猜測 Roy Fielding 的想法是,HTTP 協議已經是一個完善的應用層協議了,對於應用開發來說,只需要將所有的網絡數據抽象爲 URI 資源,然後配合可選的 Method 以及狀態碼就夠用了。
但是,在我們實際落地的情況中,HTTP 的表達力完全不夠。其限制主要有這幾個:
-
不是所有的信息都能抽象爲靜態資源,總會有一些動態行爲存在。例如 Github 的 Star 操作,即使抽象爲 star-records 也違反人的表達認知。
-
HTTP Method 無法在應用層隨意拓展,很多行爲難以被表達爲合適的 Method
-
HTTP Status 無法在應用層拓展,很多業務狀態無法使用 HTTP Status 表達
基於這些原因,人們爲了遵守 RESFul 不得不絞盡腦汁,而如果使用 RPC 風格的 API 只需要使用合適的動詞作爲 URI 以及合適的 Payload 報文格式即可。
但是,總體來說,在一定程度上,使用 RESTful 風格,可以做到自解釋性,減少了文檔的依賴。而它的缺點,也可以通過一些團隊規約避免。
本文基於技術研討會討論的內容,整理了一些使用 RESTful 的一些建議,包括如何通過團隊契約在一定程度上彌補表達力不夠的問題。
01 使用版本號解決版本不兼容問題
版本化 API 會有很多好處,而版本化 API 有很多種風格,包括:
-
使用 URL 前綴
-
使用 URL 後綴
-
使用 Header 傳參
-
使用 Query 參數
推薦使用 URL 前綴實現,這樣對後面 API 定義無侵入性且能通過 URL 表達版本。
實例:GET /v1/products/{id}
02 資源路徑參考領域模型
在一般情況下,可以以模塊、聚合根作爲路徑前綴,讓路徑排列更有規律。
參考類似模式: /[模塊]/[版本號]/[聚合根]/{id}/[實體]/{id}/[屬性/動作]
-
在微服務項目中,模塊部分可以是服務名
-
資源參考領域模型設計用詞,因此需要使用名詞複數
-
爲了彌補 RESTful 不足,允許在路徑結尾使用屬性或者動作滿足特定業務需求
-
一般 URL 路徑和文件名風格類似,使用中橫線(Dash)
-
和領域模型的層次類似,URL 層次不要太深,通過拆分小聚合實現短 URL
03 謹慎選擇 HTTP Method
HTTP 協議提供了很多的 Method,但是處於團隊理解成本的原因建議只使用下面幾個 HTTP Method:
-
GET 查詢
-
POST 新增
-
PUT 修改 / 更新
-
DELETE 刪除
避免使用 PATCH,原因是無法表達業務含義,往往產生破壞性變更。例如,將保存的出庫單提交,業務上需要修改單據的狀態。應該避免使用 Patch 部分更新單據,建議使用 PUT /xxx/submit
的形式設計,讓團隊更容易理解,保證業務一致性。
04 實體的單複數應具有實際意義
通過 URL 應該能識別出返回的結構類型是否是一個列表或者分頁的包裝對象。
例如,通過 GET /v1/orders/{id}
能猜測出返回結果是一個資源對象。
而通過 GET /v1/orders
能猜測出其結果是一個列表。
在某些項目中,複數詞彙的 URL 默認返回分頁對象,而一些項目會給分頁 URL 添加一個 page 後綴,例如 GET /v1/orders/page
。
05 合理實現冪等性
冪等含義:相同輸入,應得到相同返回。
-
GET 天然具有冪等性
-
PUT DELETE Method 應設計爲具有冪等性
-
POST 根據實際情況進行冪等性設計,例如在消息體中要求調用方傳入事務 ID 實現冪等
06 提前設計查詢語言或關鍵字
如果需要實現複雜的查詢,可以提前設計一套基於 Query 參數的查詢規則,儘可能實現通用的數據庫字段查詢。
需要考慮:
-
分頁
-
排序
-
關鍵字搜索
-
與過濾條件
-
並過濾條件
-
開閉區間查詢
這類實現一般需要結合具體數據庫查詢框架,例如 JPA、QueryDsl、Mybatis Plus 的 Wrapper 查詢能力。
例如下面一個基於 Mybatis Plus 的通用 QueryWrapper:
07 狀態碼的選用
狀態碼的選用只用於前端處理一些通用的錯誤,而具體的業務規則錯誤統一使用 409(業務規則衝突)來返回,並返回約定的錯誤碼、報錯信息。
參考如下:
-
200 請求成功
-
201 創建成功
-
400 數據校驗失敗
-
401 用戶未認證
-
403 權限檢查失敗
-
404 資源找不到
-
405 不支持的 Method
-
409 業務規則衝突
-
415 不支持的數據請求格式
-
500 服務器內部錯誤
-
503 BFF 轉發後端錯誤
08 批量處理接口
批量處理也是 RESTful 風格 API 不好處理的地方。有一些常見的做法:
-
由於 URL 使用複數已經代表資源,因此需要增加
/batch
後綴作爲 URL 區分 -
使用類似版本號的處理方式在 URL 前增加
/batch
前綴 -
URL 上可以不區分,在傳入參數的格式上區分。例如,通過傳入一個列表表達批量創建用戶。
方案 2 看似更符合 RESTful API 風格,實際上非常容易讓人困惑,因爲無法通過 URL 語義區分批量接口;方案 2 的問題是,批量接口並不常見,使用前綴會影響很多接口的語義。
在一些文章中,還會區分 bulk 和 batch 的區別,認爲 bulk 是對多個資源處理同樣的操作,而 batch 是針對多個資源處理不同的操作。
當然在實踐中我們不用如此區分,但在微服務環境下一些基礎服務往往需要提供批量接口,避免循環調用帶來的性能問題。
一些分頁接口的例子:
-
批量創建訂單
POST /v1/orders/batch
-
批量提交訂單
POST /v1/orders/batch-submit
09 動詞名詞化技巧和場景
有些情況下動詞 API 在充分建模的情況下也可以名詞化。
比如登錄的接口,我們在建模充分後識別到登錄行爲本質上創建了一個會話或者憑證,於是可以將:
POST /v1/users/login
改寫爲:
POST /v1/authorizes
另外一個例子,在金融領域需要對資產進行估值,看似應該使用:
POST /v1/assets/calculate
其實應該改寫爲:
POST /v1/capital-rating-transactions
每次評估後都會產生一次事務 ID。
10 區分成功和錯誤的返回結果
當 API 報錯時,有些做法是使用一個容器包裝錯誤信息和成功的消息體。
例如:
{
"code": "xxx"
"data": {}
"error": {}
}
這種做法相當於再次封裝了 payload 會顯得有些冗餘,主流的 API 設計一般是:當返回 HTTP 代碼爲 2xx 時,返回成功的對象,當使用其他代碼時返回錯誤的對象。
錯誤的對象一般包含:
-
業務意義的錯誤碼
-
錯誤消息
-
錯誤的詳細對象,用於某些特殊報錯需要返回更多信息
11 創建和更新只返回必要信息
在討論中,有一個話題是創建和更新操作是否需要返回處理後的對象?
返回的好處是,在編寫 API 測試時方便取數,另外也可以複用詳情的返回模型。
不返回的好處時,可以簡化開發工作,僅僅返回必要的字段和數據,前端需要獲取詳細數據,可以調用查看相關接口。
同時,不返回全量信息也可以在部分場景提高性能,避免組裝、傳輸過多數據。
12 可以參考的 API 規範和示例
除了前面的建議外,我們也經常參考一些真實的 API 設計規範,下面是一些常見可參考模仿的 API 設計示例:
-
https://docs.github.com/en/rest?apiVersion=2022-11-28
-
https://jsonapi.org/
-
https://wiki.onap.org/display/DW/RESTful+API+Design+Specification
參考資料
-
https://en.wikipedia.org/wiki/Representational_state_transfer
-
https://www.codementor.io/blog/batch-endpoints-6olbjay1hd
-
https://medium.com/paypal-tech/batch-an-api-to-bundle-multiple-paypal-rest-operations-6af6006e002
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IEE2MFJgbicpSWs2ffLICQ