總的來説,HTTP協議出現以來Web服務也就存在了。但是,自從雲計算出現後,才成為實現客户端與服務和數據交互的普遍方法。
作為一名開發者,我很幸運能夠在工作中使用一些仍然存在的SOAP服務。但是,我主要接觸的是REST,這是一種基於資源的API和Web服務開發架構風格。在我的職業生涯中有很大一部分時間都參與了構建、設計和使用API 的項目。我見過的大多數API 都“聲稱” 是 “符合REST原則”的——意味着遵循 REST 架構的原則和約束。但是,我也曾遇到過一些讓 REST 蒙羞的 API 例子,錯誤使用 HTTP 狀態碼、純文本響應、不一致的模式、插入端點中動詞...
因此我決定寫篇文章分享一下,在設計 REST API 時的最佳實踐。以下是關於設計優秀REST API 的一些建議、提示和指導,幫助您讓消費者(以及開發人員)滿意。
1. 學習 HTTP 基礎知識
如果你想構建一個設計良好的REST API,那麼你必須瞭解HTTP協議的基本知識。我堅信這將幫助你做出正確的設計選擇。Mozilla Developer Network文檔上關於HTTP概述是一個相當全面的參考資料,儘管如此,在REST API設計方面,以下是將HTTP應用於RESTful設計的簡要説明:
- HTTP具有動詞(操作或方法):最常見的是GET、POST、PUT、PATCH和DELETE。
- REST以資源為導向,資源由URI表示:
/library/ - 端點(endpoint)是動詞和URI的組合,例如:
GET: /books/ - 端點可以理解為對資源執行的操作。例如:
POST: /books/可能意味着“創建一本新書”。 - 高一層次來看,動詞映射到CRUD操作:
GET表示讀取,POST表示創建,PUT和PATCH表示更新,DELETE表示刪除 - 響應狀態由其狀態碼指定:
1xx 表示信息, 2xx 表示成功, 3xx 表示重定向, 4xx 表示客户端錯誤 和5xx 表示服務器錯誤
當然你還可以使用其他 HTTP 協議提供給 REST API 設計的功能 ,但這些都必須牢記在心裏。
2. 不要返回純文本
儘管並非強制規定的,但大多數REST API通常約定使用JSON作為數據格式。然而,僅返回包含JSON格式字符串的響應體是不夠好的。您還應該指定Content-Type標頭。它必須設置為application/json值。
在處理應用程序/編程客户端(例如,通過Python中的requests庫與您的API交互的另一個服務/API)時,這一點尤為重要——其中一些客户端依賴於此標頭來準確解碼響應。
3. 不要在 URI 中使用動詞
到目前為止,如果您已經理解了基本概念,那麼您會開始意識到在URI中放置動詞是不符合RESTful的,這是因為HTTP動詞應該足以準確描述正在對資源執行的操作。
示例:假設您要提供一個端點來生成和檢索一本書的封面。我將注意到:param 是一個URI參數(如ID或縮寫)的佔位符,你第一個想法可能是創建類似於這個的端點:
GET: /books/:slug/generateBookCover/
但是,在這裏GET方法在語法上足以説明我們正在獲取(“GET”)一本書的封面。所以,讓我們只使用:
GET: /books/:slug/bookCover/
同樣,對於創建新書的端點:
#Don’t do this
POST: /books/createNewBook/
#Do this
POST: /books/
4. 使用複數名詞表示資源
我們應該使用 /book/:id/ (單數) 還是 /books/:id/ (複數)?我個人建議使用複數形式。為什麼?因為它非常適合所有類型的端點。
我可以看到 GET /book/2/ 是沒問題的。但是 GET /book/ 呢?我們是在獲取圖書館裏唯一的那本書、其中幾本還是全部?為了避免這種模稜兩可的情況,讓我們保持一致(💡軟件職業建議!)並在所有地方都使用複數:
GET: /books/2/
POST: /books/
...
5. 在響應體中返回錯誤詳情
當API服務器處理錯誤時,將錯誤詳細信息包含在JSON主體中可以幫助使用者進行調試,這是是非常方便的,如果您還能説明哪些字段受到了錯誤的影響,那就更好了!
{
"error": "Invalid payload.",
"detail": {
"name": "This field is required."
}
}
6. 特別關注 HTTP 狀態碼
這一點非常重要,如果你從這篇文章中只記住一件事,那可能就是它了。
你的API最糟糕的事情莫過於返回一個帶有200 OK狀態碼的錯誤響應。
這是最差的語義,相反,應該返回一個能準確描述錯誤類型的有意義HTTP狀態碼。儘管如此,你可能還在想:“但我按照您推薦的方式,在響應體中發送了錯誤詳細信息,那麼問題出在哪裏呢?”
讓我給你講個故事吧。曾經我不得不集成一個API,它對每個響應都返回200 OK,並通過status字段來表示請求是否成功:
{
"status": "success",
"data": {}
}
儘管HTTP狀態碼返回200 OK,但我不能完全確定它有沒有處理我的請求失敗。
實際上,API可以返回如下響應:
HTTP/1.1 200 OK
Content-Type: text/html{
"status": "failure",
"data": {
"error": "Expected at least three items in the list."
}
}
因此,我必須檢查狀態代碼和臨時狀態字段,以確保一切正常後才能讀取數據。太煩人了!
這種設計真的很糟糕,因為它破壞了API與其使用者之間的信任關係,你會擔心API可能在欺騙你。所有這些都極不符合RESTful風格。那麼你應該怎麼做呢?利用HTTP狀態碼,並且只在響應體中提供錯誤詳細信息。
HTTP/1.1 400 Bad Request
Content-Type: application/json{
"error": "Expected at least three items in the list."
}
7. 你應該始終保持一致地使用 HTTP 狀態碼
一旦你掌握了HTTP狀態碼,就應該力求始終如一地使用它們。例如,如果你選擇某個POST端點返回201 Created,那麼對於每個POST端點都應使用相同的HTTP狀態碼。為什麼?因為消費者不應該擔心在哪種情況下哪個方法在哪個端點上會返回哪個狀態碼。
所以,請保持可預測性(一致性)。如果必須偏離約定,請在某處用大標誌記錄下來。通常,我遵循以下模式:
GET: 200 OK
PUT: 200 OK
POST: 201 Created
PATCH: 200 OK
DELETE: 204 No Content
8. 不要嵌套資源
您可能已經注意到,REST API處理的是資源。檢索資源列表或單個實例非常簡單,但是,當處理相關資源時會發生什麼呢?例如,假設我們想要檢索特定作者(名為Cagan)的書籍列表。基本上有兩個選擇。
第一個選項是將books資源嵌套在authors資源下面,例如:
GET: /authors/Cagan/books/
一些架構師推薦這種約定,因為它確實表示了作者與其書籍之間的一對多關係。但是,現在不再清楚您請求的是哪種類型的資源。 是作者嗎?還是書籍?...而且扁平化總比嵌套好,所以肯定有更好的方法... 確實如此!我個人建議使用查詢字符串參數直接過濾books資源:
GET: /books?author=Cagan
這顯然意味着:“獲取所有名為Cagan 的作者所寫的書”,對吧。
9. 優雅地處理尾部斜槓
關於URI是否應該有尾隨斜槓/實際上並不是一個值得爭論的問題,你只需要選擇其中一種方式(即帶或不帶尾隨斜槓),堅持使用它,並在客户端使用錯誤約定時優雅地重定向。
講個故事吧! 有一天,當我將REST API集成到我的一個項目中時,每次調用都收到HTTP 500內部錯誤。我所使用的端點看起來像這樣:
POST: /buckets
當時我非常生氣,怎麼也想不明白究竟哪裏出了問題。最後,原來是因為缺少了尾隨斜槓導致服務器出錯!於是,我開始使用:
POST: /buckets/
然後一切都順利進行了。API沒有修復,但希望您可以防止消費者遇到此類問題。專業提示:大多數基於網絡的框架(Angular、React等)都有一個選項可以優雅地重定向至帶或不帶尾隨斜槓的URL版本。找到那個選項並儘早激活。
10. 利用查詢字符串進行篩選和分頁
大多數情況下,一個簡單的端點無法滿足各種複雜的業務場景。您的用户可能希望檢索滿足特定條件的項目,或者一次只檢索少量數據以提高性能,這正是過濾和分頁功能所設計的目標。
通過過濾,消費者可以指定返回項目應具有哪些參數(或屬性)。分頁允許用户逐步獲取數據集。最簡單類型的分頁就是按頁碼進行分頁,它由page和page size確定。現在問題來了:如何將這樣的功能融入REST API?
我的答案是:使用查詢字符串(querystring)。
我認為使用查詢字符串實現分頁非常明顯。它看起來像這樣:
GET: /books?page=1&page_size=10
但對於過濾來説可能不那麼明顯。首先,你可能會想做類似以下操作以僅檢索已發佈書籍列表:
GET: /books/published/
設計問題:published 不是資源!相反,它是您要檢索數據所具備特徵。此類內容應放在查詢字符串中。因此最後, 用户可以像這樣獲取“包含20個項目、已發佈書籍第二頁”:
GET: /books?published=true&page=2&page_size=10
美觀且清晰易懂,不是嗎?
11. 瞭解401未授權和403禁止之間的區別
如果我每看到一次開發人員甚至有經驗的架構師搞砸這個問題就能得到一個25美分硬幣……在處理REST API中的安全錯誤時,很容易弄混錯誤是與身份驗證還是授權(又稱權限)相關 - 我以前總是遇到這種情況。根據不同情況,以下是我的備忘單,用於瞭解我正在處理什麼問題:
- 消費者沒有提供身份驗證憑據嗎?他們的SSO令牌是否無效/超時?👉 401 未授權。
- 消費者正確地進行了身份驗證,但他們沒有訪問資源所需的權限/適當的許可嗎?👉 403 禁止。
12. 充分利用 HTTP 202 Accepted
我認為202 Accepted是一個非常方便的替代201 Created的選項。它基本上意味着:
我,服務器,已經理解了你的請求。雖然我還沒有創建資源(尚未),但這沒問題。
有兩個主要場景,我覺得202 Accepted特別適用:
- 如果資源將在未來處理後被創建 — 例如:在某個工作/流程完成之後。
- 如果資源以某種方式已經存在,但這不應被視為錯誤。
13. 使用專門針對REST API的網絡框架
作為最後一個最佳實踐,讓我們討論這個問題:如何在您的API中實際應用最佳實踐?大多數時候,您希望建立一個快速的API,以便一些服務可以相互交互。Python開發者會選擇Flask,JavaScript開發者會選擇Node(Express),然後他們會實現一些簡單的路由來處理HTTP請求。
這種方法的問題在於,通常情況下,框架並不是針對構建REST API服務器而設計的。例如,Flask和Express都是兩個非常靈活的框架,但它們並沒有專門為幫助您構建REST API而制定。因此,在API中應用最佳實踐需要採取額外措施。而且大多數時候, 懶惰或缺乏時間意味着你不會付出努力——從而使你的消費者面臨一個古怪的API。
解決方案很簡單:使用合適工具完成任務。
各種語言中已經出現了新框架, 它們專門用於構建REST APIs。它們能夠幫助您輕鬆遵循最佳做法,並提高生產力。
在Python中, 我找到過其中之一優秀API框架就是Falcon。它與Flask一樣簡單易用,速度很快,非常適合在幾分鐘內構建REST API。
如果您更喜歡使用Django,那麼首選就是Django REST框架。雖然它不如其他框架直觀,但功能非常強大。在Node中,Restify似乎也是一個很好的選擇,儘管我還沒有嘗試過。我強烈建議您試一試這些框架,它們將幫助您構建美觀、優雅且設計精良的REST API。
結束語
我們都應該努力使API變得易於使用。無論是對於消費者,還是我們自己的開發人員同伴。我希望這篇文章能幫助你學到一些技巧,並激發出構建更好REST API的方法。對我來説,這只是歸結為良好的語義、簡單性和常識。
【Eolink 翻譯】,Eolink Apikit = API 管理 + Mock + 自動化測試 + 異常監控 + 團隊協作的一站式 API 生產平台,是一個跨平台(Windows、Mac、Linux、Browsers...)的 API 開發測試工具,支持 REST、Websocket、gRPC、TCP、UDP、SOAP等協議。
初創企業免費使用申請通道:https://easy-open-link.feishu.cn/share/base/form/shrcnpMe5dWt...