天天看點

設計 REST API 的13個最佳實踐

寫在前面

之是以翻譯這篇文章,是因為自從成為一名前端碼農之後,調接口這件事情就成為了家常便飯,并且,還伴随着無數的争論與無奈。編寫友好的

restful api

不論對于你的同僚,還是将來作為第三方服務調用接口的使用者來說,都顯得至關重要。關于

restful api

本身以及設計原則,我陸陸續續也看過很多的文章和書籍,在讀過原文後,感覺文中指出的 13 點最佳實踐還是比較全面的且具有參考意義的,是以翻譯出來分享給大家。如有錯誤,還望指正。

由于我一般傾向于意譯,關于原文中的開頭語或者一些與之無關的内容,我就省略掉了,畢竟時間是金錢,英語好并且能科學上網的朋友我建議還是看原文,以免造成了解上的誤差。

1. 了解應用于 REST 之上的 HTTP 知識

如果你想要建構設計優良的 REST API,了解一些關于 HTTP 協定的基礎知識是很有幫助的,畢竟磨刀不誤砍材工。

在 MDN 上有很多品質不錯的文檔介紹 HTTP。但是,就 REST API 設計本身而言,所涉及到的 HTTP 知識要點大概包含以下幾條:

  • HTTP 中包含動詞(或方法):

    GET

    POST

    PUT

    PATCH

    還有

    DELETE

    是最常用的。
  • REST 是面向資源的,一個資源被一個 URI 所辨別,比如

    /articles/

  • 端點(endpoint),一般指動詞與 URI 的組合,比如

    GET: /articles/

  • 一個端點可以被解釋為對某種資源進行的某個動作。比如,

    POST: /articles

    可能代表“建立一個新的 article”。
  • 在業務領域,我們常常可以将動詞和 CRUD(增删查改)關聯起來:

    GET

    代表查,

    POST

    代表增,

    PUT

    PATCH

    代表改(注: PUT 通常代表整體更新,而 PATCH 代表局部更新),而

    DELETE

    代表删。

當然了,你可以将 HTTP 協定中所提供的任何東西應用于 REST API 的設計之中,但以上這些是比較基礎的,是以時刻将它們記在腦海中是很有必要的。

2. 不要傳回純文字

雖然傳回 JSON 資料格式的資料不是 REST 架構規範強制限定的,但大多 REST API 都遵循這條準則。

但是,僅僅傳回 JSON 資料格式的資料還是不夠的,你還需要指定傳回 body 的頭部,比如

Content-Type

,它的值必須指定為

application/json

。這一點對于程式化用戶端尤為重要(比如通過 python 的

requests

子產品來與 api 進行互動)—— 這些程式是否對傳回資料進行正确解碼取決于這個頭部。

注:通常而言,對于浏覽器來說,這似乎不是問題,因為浏覽器一般都自帶内容嗅探機制,但為了保持一緻性,還是在響應中設定這個頭部比較妥當。

3. 避免在 URI 中使用動詞

如果你了解了第 1 條最佳實踐所傳達的意思,那麼你現在就會明白不要将動詞放入 REST API 的 URI 中。這是因為 HTTP 的動詞已經足以描述執行于資源的業務邏輯操作了。

舉個例子,當你想要提供一個針對某個 article 提供 banner 圖檔并傳回的接口時,可能會實作如下格式的接口:

GET: /articles/:slug/generateBanner/
           

這裡 GET 已經說明了這個接口是在做讀的操作,是以,可以簡化為:

GET: /articles/:slug/banner/
           

類似的,如果這個端口是要建立一個 article:

// 不要這麼做
POST: /articles/createNewArticle/

// 這才是最佳實踐
POST: /articles/
           

嘗試用 HTTP 的動詞來描述所涉及的業務邏輯操作。

4. 使用複數的名詞來描述資源

一些時候,使用資源的複數形式還是單數形式确實存在一定的困擾,比如使用

/article/:id/

更好還是使用

/articles/:id/

更好呢?

這裡我推薦使用後者。為什麼呢?因為複數形式可以滿足所有類型端點的需求。

單數形式的

GET /article/2/

看起來還是不錯的,但是如果是

GET /article/

呢?你能夠僅通過字面資訊來區分這個接口是傳回某個 article 還是多個呢?

是以,為了避免有單數命名造成的歧義性,并盡可能的保持一緻性,使用複數形式,比如:

GET: /articles/2/
POST: /articles/
...
           

5. 在響應中傳回錯誤詳情

當 API 伺服器處理錯誤時,如果能夠在傳回的 JSON body 中包含錯誤資訊,對于接口調用者來說,會一定程度上幫助他們完成調試。比如對于常見的送出表單,當遇到如下錯誤資訊時:

{
    "error": "Invalid payoad.",
    "detail": {
        "surname": "This field is required."
    }
}
           

接口調用者很快就是明白發生錯誤的原因。

6. 小心 status code

這一點可能是最重要、最重要、最重要的一點,可能也是這篇文章中,唯一你需要記住的那一點。

你可能知道,HTTP 中你可以傳回帶有 200 狀态碼的錯誤響應,但這是十分糟糕的。不要這麼做,你應當傳回與傳回錯誤類型相一緻的具有一定含義的狀态碼。

聰明的讀者可能會說,我按照第 5 點最佳實踐來提供足夠詳細的資訊,難道不行嗎?當然可以,不過讓我講一個故事:

我曾經使用過一個 API,對于它傳回的所有響應的狀态碼均是

200 OK

,同時通過響應資料中的

status

字段來表示目前的請求是否成功,比如:

{
    "status": "success",
    "data": {}
}
           

是以,雖然狀态碼是

200 OK

,但我卻不能絕對确定請求是否成功,事實上,當錯誤發生時,這個 API 會按如下代碼片段傳回響應:

HTTP/1.1 200 OK
Content-Type: text/html

{
    "status": "failure",
    "data": {
        "error": "Expected at least two items in list."
    }
}
           

頭部還是

text/html

,因為它同時傳回了一些 HTML 片段。

正因為這樣,我不得不在檢查響應狀态碼正确的同時,還需校驗這個具有特殊含義的

status

字段的值,才可以放心的處理響應傳回的

data

這種設計的一個真正壞處在于,它打破了接口與調用者之間的“信任”,因為你可能會擔心這個接口對你撒謊(注:言外之意就是,由于特設的字段可能會改變,是以增加了不可靠性)。

是以,使用正确的狀态碼,同時僅在響應的 body 中傳回錯誤資訊,并設定正确的頭部,比如:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "error": "Expected at least two items in list."
}
           

7. 保持 status code 的一緻性

當你掌握了正确使用狀态碼之後,就應該努力使它們具有一緻性。

比如,如果一個 POST 類型的端點傳回

201 Created

,那麼所有的 POST 端點都應傳回同樣的狀态碼。這樣做的好處在于,調用者無需在意端點傳回的狀态碼取決于某種特殊條件,也就形成了一緻性。如果有特殊情況,請在文檔中顯著地說明它們。

下面是我推薦的與動詞相對應的狀态碼:

GET: 200 OK
POST: 201 Created
PUT: 200 OK
PATCH: 200 OK
DELETE: 204 No Content
           

blog.florimondmanca.com/restful-api…

8. 不要嵌套資源

使用 REST API 擷取資源資料,通常情況下會直接擷取多個或者單個,但當我們需要擷取相關聯的資源時,該怎麼做呢?

比如說,我們期望擷取作者為某個 author 的 article 清單 —— 假設 authro 的

id=12

。這裡提供兩種方案:

第一種方案通過在 URI 中,将嵌套的資源放在所關聯的資源後邊來進行描述,比如:

GET: /authors/12/articles/
           

一些人推薦這種方案的理由是,這種形式的 URI 一定程度上描述了 author 與 article 之間的一對多關系。但與此同時,結合第 4 點最佳實踐,我們就不太能夠厘清目前端點傳回的資料到底是 author 類型還是 article 類型。

這裡有一篇文章,詳細闡述了扁平化形式優于嵌套形式,是以一定有更好的方法,這就是下面的第二種方案:

GET: /articles/?author_id=12
           

直接将篩選 article 的邏輯抽離為 querystring 即可,這樣的 URI 相比之前,更加清晰地描述了“擷取所有 author(id=12) 的 article”的意思。

9. 優雅地處理尾部斜杠

一個好的 URI 中是否應當包含尾部斜杠,并不具有探讨價值,選擇一種更傾向的風格并保持一緻性即可,同時當用戶端誤用尾部斜杠時,提供重定向響應。

我再來講我自己的一個故事。某天,我在将某個 API 端點內建到項目中,但是我總是收到

500 Internal Error

的錯誤,我調用的端點差不多看起來這樣:

POST: /entities
           

調試一段時間之後,我幾乎崩潰了,因為我根本不知道我哪裡做錯了,直到我發現伺服器之是以報 500 的錯誤,是因為我粗心丢掉了尾部斜杠(注:這種經曆人人都會遇到,我在 SF 上遇過無數次類似的問題),當我把 URI 改成:

POST: /entities/
           

之後,一切正常運轉。

當然,大多數的 web 架構都針對 URL 是否包含尾部斜杠,進行了優雅地處理并提供定制選項,如果可以的話,找到它并開啟這項功能。

10. 使用 querystring 來完成篩選和分頁功能

大部分情況下,一個簡單的端點沒有辦法滿足負責業務場景。

你的使用者可能想要擷取滿足一定條件下的某些資料集合 ,同時為了保證性能,僅僅擷取這個集合下的一個子集。換言之,這通常叫作篩選功能和分頁功能:

  • 篩選:使用者可以提供額外的屬性來控制傳回的資料集合
  • 分頁:擷取資料集合的子集,最簡單的分頁是基于分頁個數的分頁,它由

    page

    page_size

    來決定

那麼問題來了,我們如何将這兩項功能與 RESTful API 結合在一起呢?

答案當然是通過 querystring。對于分頁,很顯然使用這種方式再合适不過了,比如:

GET: /articles/?page=1&page_size=10
           

但對于篩選,你可能會犯第 8 點最佳實踐中所指出的問題,比如擷取處于 published 狀态的 article 清單:

GET: /articles/published/
           

除了之前提出的問題外,這裡還涉及一個設計上的問題,就是 published 本身不是資源,它僅僅是資源的特征,類似這種特征字段,應該将它們放到 querystring 中:

GET: /articles/?published=true&page=2&page_size=20
           

更加優雅、清晰,不是嗎?

11. 厘清 401 和 403

當我們遇到 API 中關于安全的錯誤提示時,很容易混淆這兩個不同類型的錯誤,認證和授權(比如權限相關)—— 老實講,我自己也經常搞混。

這裡是我自己總結的備忘錄,它闡述了我如何在實際情況下,區分它們:

  • 使用者是否未提供身份驗證憑據?認證是否還有效?這種類型的錯誤一般是未認證(

    401 Unauthorized

    )。
  • 使用者經過了正常的身份驗證,但沒有通路資源所需的權限?這種一般是未授權(

    403 Forbidden

12. 巧用 202 Accepted

我發現

202 Accepted

在某些場合是

201 Created

的一個非常便捷的替代方案,這個狀态碼的含義是:

伺服器已經接受了你的請求,但是到目前為止還未建立新的資源,但一切仍處于正常狀态。

我分享兩種特别适合使用

202 Accepted

狀态碼的業務場景:

  • 如果資源是經過位于将來一系列處理流程之後才建立的,比如當某項作業完成時
  • 如果資源已經存在,但這是理想狀态,是以不應該被識别為一個錯誤時

13. 采用 REST API 定制化的架構

作為最後一個最佳實踐,讓我們來探讨這樣一個問題:你如何在 API 的實施中,實踐最佳實踐呢?

通常的情況是這樣的,你想要快速建立一個 API 以便一些服務可以互相通路彼此。Python 開發者可能馬上掏出了 Flask,而 JS 開發者也不甘示弱,祭出了 Express,他們會使用實作一些簡單的 routes 來處理 HTTP 請求。

但這樣做的問題是,通常,web 架構并不是針對建構 REST API 服務而專門存在的,換言之,Flask 和 Express 是兩個十分通用的架構,但它們并非特别适合用于建構 REST API 服務。是以,你必須采取額外的步驟來實施 API 中的最佳實踐,但大多數情況下,由于懶惰或者時間緊張等因素,意味着你不會投入過多精力在這些方面 —— 然後給你的使用者提供了一個古怪的 API 端點。

解決方案十分簡單:工欲善其事,必先利其器,掌握并使用正确的工作才是最好的方案。在各種語言中,許多專門用于建構 REST API 服務的新架構已經出現了,它們可以幫助你在不犧牲生産力的情況下,輕松地完成工作,同時遵循最佳實踐。在 Python 中,我發現的最好的 API 架構之一是 Falcon。它與 Flask 一樣簡單,非常高效,十分适合建構 REST API 服務。如果你更喜歡 Django 的話,使用 Django REST Framework就足夠了,雖然架構不是那麼直覺(注:按我的了解應該是說不太容易上手,但是我不這麼認為),但功能非常強大。在 NodeJS 中,Restify 似乎也是一個不錯的選擇,盡管我還沒有嘗試過。我強烈建議你給這些架構一個機會!它們将幫助你建構規範,優雅且設計良好的 REST API 服務。

總結

我們都應緻力于讓調用 API 這件事成為一種樂趣。希望本文能使你了解到在建構更好的 REST API 服務的過程中,涉及到的一些建議和技巧。對我而言,應該把這些最佳實踐歸結為三點,分别是良好的語義,簡潔和合理性。

PS:分享一個好消息

同學,你造嗎?阿裡雲和騰訊雲已白菜價,雲伺服器低至不到300元/年。這裡有一份雲計算優惠活動清單,來不及解釋了,趕緊上車!

作者:HaoliangWu

連結:https://juejin.im/post/5c1ba471f265da61257812bf

繼續閱讀