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 動詞的覆蓋
有些客戶端只能使用GET
和POST
這兩種方法。服務器必須接受POST
模擬其他三個方法(PUT
、PATCH
、DELETE
)。
這時,客戶端發出的 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。它與302
和307
的含義一樣,也是"暫時重定向",區別在於302
和307
用於GET
請求,而303
用於POST
、PUT
和DELETE
請求。收到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" } ]} }
四、參考鏈接
- RESTful API Design: 13 Best Practices to Make Your Users Happy, by Florimond Manca
- API design, by MicroSoft Azure
(完)
文檔信息
- 版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證)
- 發表日期: 2018年10月 3日
相關文章
- 2020.02.23: RDF 和 SPARQL 初探:以維基數據爲例
維基百科有一個姐妹項目,叫做"維基數據"(Wikidata)。你可以從維基百科左側邊欄點進去。
- 2020.01.14: FFmpeg 視頻處理入門教程
FFmpeg 是視頻處理最常用的開源軟件。
- 2019.12.29: Bash 腳本如何創建臨時文件:mktemp 命令和 trap 命令教程
有時,Bash 腳本需要創建臨時文件或臨時目錄。
- 2019.12.24: 如何撤銷 Git 操作?
Git 版本管理時,往往需要撤銷某些操作。
留言(57條)
半卷書 說:
請問 RESTful API 對SEO友好嗎?由其是像 GET /authors/12?categories=2這種的url
felbry 說:
發現阮老師博客head也新加上了下border。我之前自己加過一段時間,後來覺得還是太醜了,哈哈
Alexu 說:
github的api似乎也是傾向於使用多級而不是查詢字符串,這麼說也不符合最佳實踐嗎?
Jamie 說:
上一篇REST還有印象hhh
Godruoyi 說:
大佬來寫果然深度不一樣,也歡迎大家去看看我總結的 restful api 規範
https://godruoyi.com/posts/resetful-api-design-specifications
t 說:
請教一下大家,如果遇到動詞不在常見的幾種之中,甚至是需要自定義的動詞,怎麼做比較合理?
明達 說:
422說的有點含糊,換個意思說,其實最常用的場景是服務器端表單驗證失敗
明達 說:
引用t的發言:請教一下大家,如果遇到動詞不在常見的幾種之中,甚至是需要自定義的動詞,怎麼做比較合理?
這個動詞是HTTP固定的吧,其實更多的動詞場景,我理解都可以區分成幾種,只要是獲取信息,都可以用GET,如果是在基本信息表增加記錄,就是POST,其他只要是修改,或者是修改關係表這種情況,應該都是UPDATE,update和put其實是有差別的。如果要update的行爲很多,我會在後面增加?type= 這類參數,如果要是特別直接的動作,比如upload這種,直接放在最高的級別也ok啊。 abc.com/upload
3.1 不要返回純本文
標題打錯啦
etworker 說:
請問對於登錄操作,可以用restful api的格式嗎?如果可以,對應的資源是什麼呢?
code 說:
引用etworker的發言:請問對於登錄操作,可以用restful api的格式嗎?如果可以,對應的資源是什麼呢?
POST /session
fengchang 說:
204 No Content 應該是指沒有需要返回給客戶端的內容,而不是服務端的內容已經不存在
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204
萌一秒 說:
前幾天看完您的js全棧,正好在查REST,最近您就出了,真的厲害!
tanglei 說:
我們的實踐: 400 用於表示客戶端傳參錯誤(或者不完全), 200 有可能也是不正常的響應(當然不能算是錯誤), 比如用戶名或者密碼不匹配.
xiaohuangmao 說:
通俗易懂 深受啓發
robinson 說:
關於restful風格和rpc風格的api設計和公司同事有過爭論,感覺是主義之爭,不會有什麼結果。不過關於rest風格,在實際應用中,也遇到過難處理的問題,比如,client驗證用戶名或者電話是否存在,就不知如何設計怎麼好,最後“強行”設計成:GET /users/checking(validating)?username=xx,反倒是,rpc風格,GET /users.check?username=xx是否表達力更強一些?再如,某個操作導致狀態更新,總結下,就是對於有很強的“動作”在內的api,應該如何用rest風格設計?這個問題困擾我很久了,望阮老師解惑,先在這裏謝過了。
Shuo Wang 說:
多個資源的關聯關係的變更,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
dada0z 說:
公司進行nessus掃描時,報告web server只允許使用GET和POST,不允許使用其他方法。方法覆寫也是禁止的。請問,這種衝突,應該如何解決?
Joshua 說:
@robinson:
可以看阮老師的這篇文章中7、誤區,裏面有講述服務的設計。
http://www.ruanyifeng.com/blog/2011/09/restful.html
李 說:
老師,請問,如果跨域前端能取得到http錯誤碼嗎,我們公司前端說跨域的時候只能取到200其他的取不到,所有如果真的取不到,那請問是不是比如:404的時候也要返回200,然後把錯誤信息和404錯誤碼放在數據體裏面。是嗎
Bob 說:
關於1.5節,僅僅舉了GET命令的例子,但是對二級資源做POST/PUT/DELETE的時候,是否還可以使用查詢字符串表達?
Rui 說:
對於查詢字符串,我們在應用的範式是當定位某種資源時,用多級地址,但當定義response如何返回時,用查詢字符串,比如返回是否是paginate的,最大返回多少
betty 說:
引用半卷書的發言:請問 RESTful API 對SEO友好嗎?由其是像 GET /authors/12?categories=2這種的url
這個沒關係吧,看你的頁面是服務端渲染還是前端渲染吧
robinson 說:
@Joshua
那篇文章,我也拜讀過,但還是有疑惑的,我們是可以向都是名詞化靠攏,但這個世界難道都可以“資源化”嗎?比如我遇到的問題,檢查用戶是否存在,難道一定要按用戶名查詢用戶?如果返回了用戶,那就是存在?同樣的情況還有:驗證驗證碼是否正確。還有訂單的情景,我下單後訂單狀態成爲“待發貨”,但如果按照“資源化”的思路,應該如何設計呢?“PUT /orders/{id}?action=下單”?還是PUT /orders/{id}?status=代發貨?或者/orders/{id}/status/待發貨?我感覺後面的這種情況更嚴重,這樣封裝性很差,把邏輯交給了下游,有爲了rest而rest之嫌,如果是指定action的情況,那麼也比較糟糕,難道我們對訂單的接口只有四個?其餘的都只能通過參數表達?後端實現也會成爲一鍋粥。還望各位大牛解惑
陳生 說:
感覺沒看懂呀。。。
槍騎兵叔叔 說:
勘誤下:
3.2裏 “Invalid payoad.”
是payload吧,單詞拼寫錯誤
Lightc 說:
引用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操作
not3 說:
PUT /user/${user_id}.join-to/team/${team_id}
是否可以
英武 說:
怎麼看都覺得少了點什麼,也許功能測試都沒有什麼問題,各種cornner都要測到,但是性能測試可否詳細談一下?locust?
not3 說:
比如獲取某個作者的某一類文章。
這個例子寫的示例語義上不太好,返回的資源其實是文章,那麼應該表述爲
GET /articles?authorId=12&categoryId=2
本來就沒有層級關係
另外,某類的所有文章,某作者的所有文章
GET /category/2/articles
GET /author/12/articles
binger 說:
有個疑問,發生錯誤了狀態碼不能爲200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼麼。。。
鄭誠 說:
爲什麼沒有502
Xanthuim 說:
引用李的發言:老師,請問,如果跨域前端能取得到http錯誤碼嗎,我們公司前端說跨域的時候只能取到200其他的取不到,所有如果真的取不到,那請問是不是比如:404的時候也要返回200,然後把錯誤信息和404錯誤碼放在數據體裏面。是嗎
怎麼可能取不到,只要是基於http協議的都可以。只是他們沒有這麼做,要麼是前端技術low,對於這種你就把這篇文章丟給他即可,其他什麼都不要說。
Xanthuim 說:
引用鄭誠 的發言:爲什麼沒有502
文章都說的很清楚,對於服務端異常,一般不會透露過多的信息:
5xx狀態碼錶示服務端錯誤。一般來說,API 不會向用戶透露服務器的詳細信息,所以只要兩個狀態碼就夠了。
當然你也要把更多的異常信息往外拋,看你了,只是不建議。
Xanthuim 說:
引用binger的發言:有個疑問,發生錯誤了狀態碼不能爲200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼麼。。。
你可以返回實際的狀態碼,比如你現在要返回的HTTP狀態碼是404,那麼返回的JSON中狀態碼也可以用404,其他也是類似的。
Xanthuim 說:
@Shuo Wang:
你這種就不應該放在一起,分開寫
Xanthuim 說:
引用fengchang的發言:204 No Content 應該是指沒有需要返回給客戶端的內容,而不是服務端的內容已經不存在
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204
刪除了是沒有啊,表示這個資源已經不存在,用204沒毛病。其實沒必要太鑽牛角尖,能基本表示清楚就可以了。
有一點夢想的鹹魚 說:
api呈現給用戶可以繼承swgger。哈哈哈哈......手寫API文檔的歲月一去不復返
pto2 說:
@robinson:
你把“驗證存在性”理解爲“嘗試獲取”就好辦了,直接GET /users/你想獲取的用戶名 ,不存在就直接返回不存在就是了。
血火 說:
老阮的你可真是功德無量
monch 說:
請問 比如獲取 最後一篇文章的api怎麼設計
首先,要的是文章 api應該這樣寫,/api/articles/
但是,已經確定了要的是一篇文章,所以不應該以數組的形式返回了吧,但是又不知道最後一篇的id
所以類似這種的api怎麼寫呢
(不知道ID,然後加了條件,只需要返回單個資源)
王瑞芳 說:
老師,您有課程嗎?在哪裏可以看
路過看看 說:
引用binger的發言:有個疑問,發生錯誤了狀態碼不能爲200,應該給出具體狀態碼,錯誤放在返回值中,反正都是要解析返回值的,狀態碼200不是少判斷一步狀態碼麼。。。
這裏的錯誤是指的http類型的錯誤,而不是你的業務邏輯錯誤,業務邏輯的錯誤還是需要自行約定code
高媛 說:
老師,有出系統的前端全棧培訓嗎,很喜歡老師的文章
Haven 說:
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。
鄭 說:
請問一下如果(網頁 ,前後端分離)我想要一週的數據,怎樣設計? 是前端處理嗎?
leoskey 說:
引用小北的發言:比如對一條記錄有多種動作怎麼做呢?
是:
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}" 去進行更新
lalio 說:
引用旺旺大饅頭的發言:@robinson:
關於下單這個,首先,資源是訂單,那麼你下單其實是新增一個訂單資源,那就是"POST /orders",待發貨這些只是訂單的一個屬性,後續應該是通過"PUT /orders/{orderId}" 去進行更新
這樣對後端實現不友好,例如,下單,退訂,支付,這三個都是比較大的場景,按照你的理解就是全都有這一個接口去完成了。"PUT /orders/{orderId}"
個人感覺POST /orders/下單 、 POST /orders/退訂、 POST /orders/支付,這樣是更好的設計,但是這幾個場景都是很強的動詞語境,沒法名詞化,不符合RESTFUL了。
哈哈 說:
GET /authors/12?categories=2
這種就不算是RESTful風格的了吧
只能說是API了
mzghm 說:
阮工的文章總是言簡意賅,讀起來順暢清晰
我是一隻小小鳥 說:
我也存在和訂單類似的問題,比如是用戶的啓用與禁用,接口該如何設計呢?是PUT /users/{id}/enbale 還是 PUT /users/{id}/status?status_value=enbale,我個人是更傾向於前者的,至少表達清晰,通過接口就能知道是幹啥。
另外,還有批量啓用和禁用這類的批量操作該如何定義和設計呢?此時用PUT /users/{id}/enbale這個也不合適了。望解答。