RESTful API 最佳實踐

作者: 阮一峰

日期: 2018年10月 3日

RESTful 是目前最流行的 API 設計規范,用于 Web 數據接口的設計。

它的大原則容易把握,但是細節不容易做對。本文總結 RESTful 的設計細節,介紹如何設計出易于理解和使用的 API。

一、URL 設計

1.1 動詞 + 賓語

RESTful 的核心思想就是,客戶端發出的數據操作指令都是"動詞 + 賓語"的結構。比如,GET /articles這個命令,GET是動詞,/articles是賓語。

動詞通常就是五種 HTTP 方法,對應 CRUD 操作。

  • GET:讀取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常是部分更新
  • DELETE:刪除(Delete)

根據 HTTP 規范,動詞一律大寫。

1.2 動詞的覆蓋

有些客戶端只能使用GETPOST這兩種方法。服務器必須接受POST模擬其他三個方法(PUTPATCHDELETE)。

這時,客戶端發出的 HTTP 請求,要加上X-HTTP-Method-Override屬性,告訴服務器應該使用哪一個動詞,覆蓋POST方法。


POST /api/Person/4 HTTP/1.1  
X-HTTP-Method-Override: PUT

上面代碼中,X-HTTP-Method-Override指定本次請求的方法是PUT,而不是POST

1.3 賓語必須是名詞

賓語就是 API 的 URL,是 HTTP 動詞作用的對象。它應該是名詞,不能是動詞。比如,/articles這個 URL 就是正確的,而下面的 URL 不是名詞,所以都是錯誤的。

  • /getAllCars
  • /createNewCar
  • /deleteAllRedCars

1.4 復數 URL

既然 URL 是名詞,那么應該使用復數,還是單數?

這沒有統一的規定,但是常見的操作是讀取一個集合,比如GET /articles(讀取所有文章),這里明顯應該是復數。

為了統一起見,建議都使用復數 URL,比如GET /articles/2要好于GET /article/2

1.5 避免多級 URL

常見的情況是,資源需要多級分類,因此很容易寫出多級的 URL,比如獲取某個作者的某一類文章。


GET /authors/12/categories/2

這種 URL 不利于擴展,語義也不明確,往往要想一會,才能明白含義。

更好的做法是,除了第一級,其他級別都用查詢字符串表達。


GET /authors/12?categories=2

下面是另一個例子,查詢已發布的文章。你可能會設計成下面的 URL。


GET /articles/published

查詢字符串的寫法明顯更好。


GET /articles?published=true

二、狀態碼

2.1 狀態碼必須精確

客戶端的每一次請求,服務器都必須給出回應。回應包括 HTTP 狀態碼和數據兩部分。

HTTP 狀態碼就是一個三位數,分成五個類別。

  • 1xx:相關信息
  • 2xx:操作成功
  • 3xx:重定向
  • 4xx:客戶端錯誤
  • 5xx:服務器錯誤

這五大類總共包含100多種狀態碼,覆蓋了絕大部分可能遇到的情況。每一種狀態碼都有標準的(或者約定的)解釋,客戶端只需查看狀態碼,就可以判斷出發生了什么情況,所以服務器應該返回盡可能精確的狀態碼。

API 不需要1xx狀態碼,下面介紹其他四類狀態碼的精確含義。

2.2 2xx 狀態碼

200狀態碼表示操作成功,但是不同的方法可以返回更精確的狀態碼。

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

上面代碼中,POST返回201狀態碼,表示生成了新的資源;DELETE返回204狀態碼,表示資源已經不存在。

此外,202 Accepted狀態碼表示服務器已經收到請求,但還未進行處理,會在未來再處理,通常用于異步操作。下面是一個例子。


HTTP/1.1 202 Accepted

{
  "task": {
    "href": "/api/company/job-management/jobs/2130040",
    "id": "2130040"
  }
}

2.3 3xx 狀態碼

API 用不到301狀態碼(永久重定向)和302狀態碼(暫時重定向,307也是這個含義),因為它們可以由應用級別返回,瀏覽器會直接跳轉,API 級別可以不考慮這兩種情況。

API 用到的3xx狀態碼,主要是303 See Other,表示參考另一個 URL。它與302307的含義一樣,也是"暫時重定向",區別在于302307用于GET請求,而303用于POSTPUTDELETE請求。收到303以后,瀏覽器不會自動跳轉,而會讓用戶自己決定下一步怎么辦。下面是一個例子。


HTTP/1.1 303 See Other
Location: /api/orders/12345

2.4 4xx 狀態碼

4xx狀態碼表示客戶端錯誤,主要有下面幾種。

400 Bad Request:服務器不理解客戶端的請求,未做任何處理。

401 Unauthorized:用戶未提供身份驗證憑據,或者沒有通過身份驗證。

403 Forbidden:用戶通過了身份驗證,但是不具有訪問資源所需的權限。

404 Not Found:所請求的資源不存在,或不可用。

405 Method Not Allowed:用戶已經通過身份驗證,但是所用的 HTTP 方法不在他的權限之內。

410 Gone:所請求的資源已從這個地址轉移,不再可用。

415 Unsupported Media Type:客戶端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客戶端要求返回 XML 格式。

422 Unprocessable Entity :客戶端上傳的附件無法處理,導致請求失敗。

429 Too Many Requests:客戶端的請求次數超過限額。

2.5 5xx 狀態碼

5xx狀態碼表示服務端錯誤。一般來說,API 不會向用戶透露服務器的詳細信息,所以只要兩個狀態碼就夠了。

500 Internal Server Error:客戶端請求有效,服務器處理時發生了意外。

503 Service Unavailable:服務器無法處理請求,一般用于網站維護狀態。

三、服務器回應

3.1 不要返回純本文

API 返回的數據格式,不應該是純文本,而應該是一個 JSON 對象,因為這樣才能返回標準的結構化數據。所以,服務器回應的 HTTP 頭的Content-Type屬性要設為application/json

客戶端請求時,也要明確告訴服務器,可以接受 JSON 格式,即請求的 HTTP 頭的ACCEPT屬性也要設成application/json。下面是一個例子。


GET /orders/2 HTTP/1.1 
Accept: application/json

3.2 發生錯誤時,不要返回 200 狀態碼

有一種不恰當的做法是,即使發生錯誤,也返回200狀態碼,把錯誤信息放在數據體里面,就像下面這樣。


HTTP/1.1 200 OK
Content-Type: application/json

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

上面代碼中,解析數據體以后,才能得知操作失敗。

這張做法實際上取消了狀態碼,這是完全不可取的。正確的做法是,狀態碼反映發生的錯誤,具體的錯誤信息放在數據體里面返回。下面是一個例子。


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

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

3.3 提供鏈接

API 的使用者未必知道,URL 是怎么設計的。一個解決方法就是,在回應中,給出相關鏈接,便于下一步操作。這樣的話,用戶只要記住一個 URL,就可以發現其他的 URL。這種方法叫做 HATEOAS。

舉例來說,GitHub 的 API 都在 api.github.com 這個域名。訪問它,就可以得到其他 URL。


{
  ...
  "feeds_url": "https://api.github.com/feeds",
  "followers_url": "https://api.github.com/user/followers",
  "following_url": "https://api.github.com/user/following{/target}",
  "gists_url": "https://api.github.com/gists{/gist_id}",
  "hub_url": "https://api.github.com/hub",
  ...
}

上面的回應中,挑一個 URL 訪問,又可以得到別的 URL。對于用戶來說,不需要記住 URL 設計,只要從 api.github.com 一步步查找就可以了。

HATEOAS 的格式沒有統一規定,上面例子中,GitHub 將它們與其他屬性放在一起。更好的做法應該是,將相關鏈接與其他屬性分開。


HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "In progress",
   "links": {[
    { "rel":"cancel", "method": "delete", "href":"/api/status/12345" } ,
    { "rel":"edit", "method": "put", "href":"/api/status/12345" }
  ]}
}

四、參考鏈接

(完)

留言(55條)

請問 RESTful API 對SEO友好嗎?由其是像 GET /authors/12?categories=2這種的url

發現阮老師博客head也新加上了下border。我之前自己加過一段時間,后來覺得還是太丑了,哈哈

github的api似乎也是傾向于使用多級而不是查詢字符串,這么說也不符合最佳實踐嗎?

上一篇REST還有印象hhh

大佬來寫果然深度不一樣,也歡迎大家去看看我總結的 restful api 規范

https://godruoyi.com/posts/resetful-api-design-specifications

請教一下大家,如果遇到動詞不在常見的幾種之中,甚至是需要自定義的動詞,怎么做比較合理?

422說的有點含糊,換個意思說,其實最常用的場景是服務器端表單驗證失敗

引用t的發言:

請教一下大家,如果遇到動詞不在常見的幾種之中,甚至是需要自定義的動詞,怎么做比較合理?

這個動詞是HTTP固定的吧,其實更多的動詞場景,我理解都可以區分成幾種,只要是獲取信息,都可以用GET,如果是在基本信息表增加記錄,就是POST,其他只要是修改,或者是修改關系表這種情況,應該都是UPDATE,update和put其實是有差別的。如果要update的行為很多,我會在后面增加?type= 這類參數,如果要是特別直接的動作,比如upload這種,直接放在最高的級別也ok啊。 abc.com/upload

3.1 不要返回純本文
標題打錯啦

請問對于登錄操作,可以用restful api的格式嗎?如果可以,對應的資源是什么呢?

引用etworker的發言:

請問對于登錄操作,可以用restful api的格式嗎?如果可以,對應的資源是什么呢?

POST /session

204 No Content 應該是指沒有需要返回給客戶端的內容,而不是服務端的內容已經不存在

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204

前幾天看完您的js全棧,正好在查REST,最近您就出了,真的厲害!

我們的實踐: 400 用于表示客戶端傳參錯誤(或者不完全), 200 有可能也是不正常的響應(當然不能算是錯誤), 比如用戶名或者密碼不匹配.

通俗易懂 深受啟發

關于restful風格和rpc風格的api設計和公司同事有過爭論,感覺是主義之爭,不會有什么結果。不過關于rest風格,在實際應用中,也遇到過難處理的問題,比如,client驗證用戶名或者電話是否存在,就不知如何設計怎么好,最后“強行”設計成:GET /users/checking(validating)?username=xx,反倒是,rpc風格,GET /users.check?username=xx是否表達力更強一些?再如,某個操作導致狀態更新,總結下,就是對于有很強的“動作”在內的api,應該如何用rest風格設計?這個問題困擾我很久了,望阮老師解惑,先在這里謝過了。

多個資源的關聯關系的變更,URL 如何設計比較好?

比如將某個用戶加入到某個 Team 中

PUT /users/${user_id}/teams/${team_id}
PUT /users/${user_id}?team_id=${team_id}

如果是第二種,是不是不太好區分 users 和 teams 是兩種資源?如果是第一種,就會比較明確一些。

又或者將某個 Team 中的某個用戶設置為非激活狀態

PUT /users/${user_id}/teams/${team_id}?status=inactive
PUT /users/${user_id}?team_id=${team_id}?status=inactive

公司進行nessus掃描時,報告web server只允許使用GET和POST,不允許使用其他方法。方法覆寫也是禁止的。請問,這種沖突,應該如何解決?

@robinson:

可以看阮老師的這篇文章中7、誤區,里面有講述服務的設計。
http://www.clmqnq.live/blog/2011/09/restful.html

老師,請問,如果跨域前端能取得到http錯誤碼嗎,我們公司前端說跨域的時候只能取到200其他的取不到,所有如果真的取不到,那請問是不是比如:404的時候也要返回200,然后把錯誤信息和404錯誤碼放在數據體里面。是嗎

關于1.5節,僅僅舉了GET命令的例子,但是對二級資源做POST/PUT/DELETE的時候,是否還可以使用查詢字符串表達?

對于查詢字符串,我們在應用的范式是當定位某種資源時,用多級地址,但當定義response如何返回時,用查詢字符串,比如返回是否是paginate的,最大返回多少

引用半卷書的發言:

請問 RESTful API 對SEO友好嗎?由其是像 GET /authors/12?categories=2這種的url

這個沒關系吧,看你的頁面是服務端渲染還是前端渲染吧

@Joshua

那篇文章,我也拜讀過,但還是有疑惑的,我們是可以向都是名詞化靠攏,但這個世界難道都可以“資源化”嗎?比如我遇到的問題,檢查用戶是否存在,難道一定要按用戶名查詢用戶?如果返回了用戶,那就是存在?同樣的情況還有:驗證驗證碼是否正確。還有訂單的情景,我下單后訂單狀態成為“待發貨”,但如果按照“資源化”的思路,應該如何設計呢?“PUT /orders/{id}?action=下單”?還是PUT /orders/{id}?status=代發貨?或者/orders/{id}/status/待發貨?我感覺后面的這種情況更嚴重,這樣封裝性很差,把邏輯交給了下游,有為了rest而rest之嫌,如果是指定action的情況,那么也比較糟糕,難道我們對訂單的接口只有四個?其余的都只能通過參數表達?后端實現也會成為一鍋粥。還望各位大牛解惑

感覺沒看懂呀。。。

勘誤下:
3.2里 “Invalid payoad.”
是payload吧,單詞拼寫錯誤

引用robinson的發言:

關于restful風格和rpc風格的api設計和公司同事有過爭論,感覺是主義之爭,不會有什么結果。不過關于rest風格,在實際應用中,也遇到過難處理的問題,比如,client驗證用戶名或者電話是否存在,就不知如何設計怎么好,最后“強行”設計成:GET /users/checking(validating)?username=xx,反倒是,rpc風格,GET /users.check?username=xx是否表達力更強一些?再如,某個操作導致狀態更新,總結下,就是對于有很強的“動作”在內的api,應該如何用rest風格設計?這個問題困擾我很久了,望阮老師解惑,先在這里謝過了。

我覺得這樣的設計成這樣比較 GET /users/{userName}?c=check ,c代表command的意思,對userName進行check操作

PUT /user/${user_id}.join-to/team/${team_id}
是否可以

怎么看都覺得少了點什么,也許功能測試都沒有什么問題,各種cornner都要測到,但是性能測試可否詳細談一下?locust?

比如獲取某個作者的某一類文章。

這個例子寫的示例語義上不太好,返回的資源其實是文章,那么應該表述為

GET /articles?authorId=12&categoryId=2

本來就沒有層級關系

另外,某類的所有文章,某作者的所有文章
GET /category/2/articles
GET /author/12/articles

有個疑問,發生錯誤了狀態碼不能為200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼么。。。

為什么沒有502

引用李的發言:

老師,請問,如果跨域前端能取得到http錯誤碼嗎,我們公司前端說跨域的時候只能取到200其他的取不到,所有如果真的取不到,那請問是不是比如:404的時候也要返回200,然后把錯誤信息和404錯誤碼放在數據體里面。是嗎

怎么可能取不到,只要是基于http協議的都可以。只是他們沒有這么做,要么是前端技術low,對于這種你就把這篇文章丟給他即可,其他什么都不要說。

引用鄭誠 的發言:

為什么沒有502

文章都說的很清楚,對于服務端異常,一般不會透露過多的信息:
5xx狀態碼表示服務端錯誤。一般來說,API 不會向用戶透露服務器的詳細信息,所以只要兩個狀態碼就夠了。

當然你也要把更多的異常信息往外拋,看你了,只是不建議。

引用binger的發言:

有個疑問,發生錯誤了狀態碼不能為200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼么。。。

你可以返回實際的狀態碼,比如你現在要返回的HTTP狀態碼是404,那么返回的JSON中狀態碼也可以用404,其他也是類似的。

@Shuo Wang:

你這種就不應該放在一起,分開寫

引用fengchang的發言:

204 No Content 應該是指沒有需要返回給客戶端的內容,而不是服務端的內容已經不存在

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204

刪除了是沒有啊,表示這個資源已經不存在,用204沒毛病。其實沒必要太鉆牛角尖,能基本表示清楚就可以了。

api呈現給用戶可以繼承swgger。哈哈哈哈......手寫API文檔的歲月一去不復返

@robinson:

你把“驗證存在性”理解為“嘗試獲取”就好辦了,直接GET /users/你想獲取的用戶名 ,不存在就直接返回不存在就是了。

老阮的你可真是功德無量

請問 比如獲取 最后一篇文章的api怎么設計

首先,要的是文章 api應該這樣寫,/api/articles/
但是,已經確定了要的是一篇文章,所以不應該以數組的形式返回了吧,但是又不知道最后一篇的id


所以類似這種的api怎么寫呢
(不知道ID,然后加了條件,只需要返回單個資源)

老師,您有課程嗎?在哪里可以看

引用binger的發言:

有個疑問,發生錯誤了狀態碼不能為200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼么。。。

這里的錯誤是指的http類型的錯誤,而不是你的業務邏輯錯誤,業務邏輯的錯誤還是需要自行約定code

老師,有出系統的前端全棧培訓嗎,很喜歡老師的文章

1.4 復數 URL

既然 URL 是名詞,那么應該使用復數,還是單數?

這沒有統一的規定,但是常見的操作是讀取一個集合,比如GET /articles(讀取所有文章),這里明顯應該是復數。

為了統一起見,建議都使用復數 URL,比如GET /articles/2要好于GET /article/2。

------------------------------------

其實這里挺難說服我的,在DELETE、PUT、PATCH、GET(獲得單條數據)這些接口基本都是操作單條數據的,應使用單數。而只有列表一個接口是多條數據,使用復數。那按照少數服從多數( - _-),應該使用單數才對。

感覺要客戶端去判斷數據是應該用post創建還是用put/patch去修改有些麻煩,特別是客戶端數據結構比較多的情況下。我個人傾向于一個post包打天下,不管是創建還是更新,都用只用post方法。這樣雖然不是那么符合規范,但是實現起來相對比較容易。

學習了
3.2部分 第三段有別字

比如對一條記錄有多種動作怎么做呢?

是:

POST /datas/1?action=reportError
POST /datas/1?action=mark
POST /datas/1?action=assign

還是:

POST /datas/1/reportError
POST /datas/1/mark
POST /datas/1/assign

個人覺得下面這樣更清晰,且我不需要在接口函數中判斷參數寫if else。

請問一下如果(網頁 ,前后端分離)我想要一周的數據,怎樣設計? 是前端處理嗎?

引用小北的發言:

比如對一條記錄有多種動作怎么做呢?

是:

POST /datas/1?action=reportError
POST /datas/1?action=mark
POST /datas/1?action=assign

還是:

POST /datas/1/reportError
POST /datas/1/mark
POST /datas/1/assign

個人覺得下面這樣更清晰,且我不需要在接口函數中判斷參數寫if else。

看了下 Github 的 star ,采用的是第二種

@robinson:

關于下單這個,首先,資源是訂單,那么你下單其實是新增一個訂單資源,那就是"POST /orders",待發貨這些只是訂單的一個屬性,后續應該是通過"PUT /orders/{orderId}" 去進行更新

引用旺旺大饅頭的發言:

@robinson:

關于下單這個,首先,資源是訂單,那么你下單其實是新增一個訂單資源,那就是"POST /orders",待發貨這些只是訂單的一個屬性,后續應該是通過"PUT /orders/{orderId}" 去進行更新

這樣對后端實現不友好,例如,下單,退訂,支付,這三個都是比較大的場景,按照你的理解就是全都有這一個接口去完成了。"PUT /orders/{orderId}"

個人感覺POST /orders/下單 、 POST /orders/退訂、 POST /orders/支付,這樣是更好的設計,但是這幾個場景都是很強的動詞語境,沒法名詞化,不符合RESTFUL了。

GET /authors/12?categories=2
這種就不算是RESTful風格的了吧
只能說是API了

阮工的文章總是言簡意賅,讀起來順暢清晰

我也存在和訂單類似的問題,比如是用戶的啟用與禁用,接口該如何設計呢?是PUT /users/{id}/enbale 還是 PUT /users/{id}/status?status_value=enbale,我個人是更傾向于前者的,至少表達清晰,通過接口就能知道是干啥。
另外,還有批量啟用和禁用這類的批量操作該如何定義和設計呢?此時用PUT /users/{id}/enbale這個也不合適了。望解答。

我要發表看法

«-必填

«-必填,不公開

«-我信任你,不會填寫廣告鏈接

3d字谜工作室