REST API 簡介
REST 是 Representational State Transfer 的縮寫,它將資源作為核心概念,通過 HTTP 方法對資源進行操作。其本身是一套圍繞資源進行操作的架構規范。在實際應用中,更多的是體現在 API 的設計上。
企業在進行產品設計開發時,通常首先由業務專家和技術專家一起梳理出業務模型,然后根據領域驅動設計(DDD)的方法論進行建模,設計出領域模型以及針對領域模型的操作。最終,這些領域模型會映射為數據存儲的數據模型以及 REST API 的資源模型,而針對領域模型的操作會映射為 HTTP 方法以及 REST API 的 Action。
REST API 幾乎已經是互聯網服務 Web API 設計的事實標準,根據 Google 的 API 設計指南,早在 2010 年,就有大約 74% 的公共網絡 API 是 HTTP REST(或類似 REST)風格的設計,大多數 API 均使用 JSON 作為傳輸格式。
RESTful 設計原則
滿足 REST 要求的架構需遵循以下6個設計原則:
1. 客戶端與服務端分離
目的是將客戶端和服務端的關注點分離。在 Web 應用中,將用戶界面所關注的邏輯和服務端數據存儲所關注的邏輯分離開來,有助于提高客戶端的跨平臺的可移植性;也有助于提高服務端的可擴展性。
隨著前端技術的發展,前后端分離已經是主流的開發方式,傳統的 Spring MVC/Django 的前端模板渲染已經被逐漸棄用了。
2. 無狀態
服務端不保存客戶端的上下文信息,會話信息由客戶端保存,服務端根據客戶端的請求信息處理請求。
在實際開發中,服務端通常會保存一些狀態信息,比如會話信息、認證信息等,這些信息一般是保存在服務端的數據庫或者緩存中。
3. 可緩存
這一條算是上一條的延伸,無狀態服務提升了系統的可靠性、可擴展性,但也會造成不必要的網絡開銷。為了緩解這個問題,REST 要求客戶端或者中間代理(網關)能緩存服務端的響應數據。服務端的響應信息必須明確表示是否可以被緩存以及緩存的時長,以避免客戶端請求到過期數據。
管理良好的緩存機制可以有效減少客戶端-服務器之間的交互,甚至完全避免客戶端-服務器交互,從而提升了系統的性能和可擴展性。
4. 分層系統
對于客戶端來說,中間代理是透明的。客戶端無需知道請求路徑中代理、網關、負載均衡等中間件的存在,這樣可以提高系統的可擴展性和安全性。
5. 統一接口
REST 要求開發者面向資源來設計系統,有下面四個約束:
-
每次請求中都包含資源 ID
-
所有操作均等通過資源 ID 進行
-
消息是自描述的:每條消息包含足夠的信息來描述如何處理這條消息。比如 mime 標識媒體類型,content-type 標識編碼格式,language 標識語言,charset 標識字符集,encoding 標識壓縮格式等。
-
用超媒體驅動應用狀態(HATEOAS,Hypermedia as the Engine of Application State):客戶端在訪問了最初的 REST API 后,服務端會返回后續操作的鏈接,客戶端使用服務端提供的鏈接動態的發現可用資源和可執行操作。
6. 按需編碼(可選)
這是一條可選約束,指的是服務端可以根據客戶端需求,將可執行代碼發送給客戶端,從而實現臨時性的功能擴展或定制功能,比如以前的 Java Applet。
REST API 成熟度模型
上述約束讀起來還是有些抽象,鑒于在實際開發中,我們更多是聚焦在 API 設計上。為了衡量一個系統是否符合 REST 風格,《RESTful Web APIs》和《RESTful Web Services》的作者 Leonard Richardson 提出了 REST 成熟度模型,根據 API 的設計風格將其分為了 4 級。
第 0 級: 完全不符合 REST 風格
比如 RPC 面向過程的 API 設計基本是圍繞操作過程來設計的,完全沒有資源的概念。
下面是 Martin Fowler 在介紹成熟度模型的 blog Richardson Maturity Model 中舉的病人預約的例子,病人首先需要查詢醫生可預約的時間表,然后提交預約。
查詢預約服務時提交的請求為
POST /appointmentService?action=query HTTP/1.1{"date": "2020-03-04","doctor": "mjones"
}
請求成功后響應如下
HTTP/1.1 200 OK
[{"start": "14:00","end": "14:50","doctor": "mjones"},{"start": "16:00","end": "16:50","doctor": "mjones"}
]
然后病人選擇時段提交預約
POST /appointmentService?action=confirm HTTP/1.1{"slot": {"start": "14:00","end": "14:50","doctor": "mjones"},"patient": {"id": "jsmith"}
}
預定成功時響應如下
HTTP/1.1 200 OK{"slot": {"start": "14:00","end": "14:50","doctor": "mjones"},"patient": {"id": "jsmith"}
}
預定失敗時響應如下
HTTP/1.1 200 OK{"slot": {"start": "14:00","end": "14:50","doctor": "mjones"},"patient": {"id": "jsmith"},"reason": "Slot not available"
}
可以看到整個請求過程沒有涉及到資源的概念,并且請求也比較簡潔明了。但如果操作越來越多,接口也越來越多,隨之而來的維護、溝通成本也會越來越高。
第 1 級:引入資源概念
引入資源后,對服務端的訪問都是圍繞資源,通過資源 ID 進行。此時的查詢和預約請求如下:
查詢預約:以醫生為資源,通過 ID查詢
POST /doctors/mjones HTTP/1.1{date: "2020-03-04"}// 請求響應[{"slot_id": 1234, doctor: "mjones", start: "14:00", end: "14:50"},{"slot_id": 5678, doctor: "mjones", start: "16:00", end: "16:50"}
]
提交預約時,以時間表 slot 為資源,通過 ID 預約
POST /slots/1234 HTTP/1.1{ "patient_id": "jsmith" }
第 2 級:操作映射到 HTTP 方法
上面的例子中所有請求都是用的 POST 方法,Level2 要求將操作映射到 HTTP 方法。對于資源的操作無非就是增刪改查,HTTP 對應的 POST、DELETE、PUT/PATCH、GET 可以很好的表達這些操作。
- 查詢檔期,使用 GET 方法
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.[{"slot_id": 1234, doctor: "mjones", start: "14:00", end: "14:50"},{"slot_id": 5678, doctor: "mjones", start: "16:00", end: "16:50"}
]
- 創建預約,使用 POST 方法
POST /schedules/1234 HTTP/1.1{ "patient_id": "jsmith" }
// 預定成功響應
HTTP/1.1 201 Created
Location: slots/1234/appointment{"slot": {"id": 1234,"doctor": "mjones","start": "14:00","end": "14:50"},"patient": {"id": "jsmith"}
}
預定失敗時,需要返回能表達錯誤原因的響應碼,而不是像之前一樣返回 200。
HTTP/1.1 409 Conflict[{"slot_id": 5678, doctor: "mjones", start: "16:00", end: "16:50"}
]
第2級是目前絕大多數系統所達到的級別。
第 3 級:狀態轉移完全由后端驅動
在實際開發中,通常是客戶端和服務端約定好 API 后進行各自的實現。客戶端在代碼中已經編寫了 API 相關的調用,但 REST 認為這是多余的,客戶端應該根據服務端返回的鏈接進行后續操作,返回的資源信息以及操作鏈接信息能夠描述自身以及后續可能發生的狀態轉移,從而實現超文本驅動應用狀態。
依然是查詢預約的 API,此時后端返回的預約列表,除了基本信息外還帶有預約所需 link,由此客戶端知曉后續的預約操作,并請求服務端返回的 link 進行操作。
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1[{"slot_id": 1234, doctor: "mjones", start: "14:00", end: "14:50", links: [{"rel": "book", "href": "/slots/1234"}]},{"slot_id": 5678, doctor: "mjones", start: "16:00", end: "16:50", links: [{"rel": "book", "href": "/slots/5678"}]}
]
可以看到返回的數據中包含了支持的預約操作以及操作所對應的鏈接。
REST VS RPC
API 的設計通常有 RPC 和 REST 兩種形式。雖然兩者并不是一回事,但因為都是面向服務端和客戶端的通信制定規范,所以經常被混為一談。
REST 本身一套面向資源的架構設計思想,而 RPC 的初衷是希望能在分布式系統之間,像調用本地方法一樣調用遠程方法,圍繞通信過程實現進行的一系列實現。RPC 協議也是層出不窮,針對數據的編碼、傳輸以及方法的表達提供不同的解決方案。關于 RPC 更多的講解可以參考鳳凰架構:遠程服務調用。
具體到 API 設計上,其主要區別在于:REST 是面向資源的,而 RPC 是面向過程的。以一個用戶的增刪改查為例,REST 的 API 設計如下
# 創建用戶
POST /users
# 查詢用戶列表
GET /users
# 查詢用戶詳情
GET /users/{id}
# 更新用戶信息
PUT /users/{id}
# 刪除用戶
DELETE /users/{id}
而 RPC 的 API 設計如下:
# 創建用戶
POST /createUser
# 查詢用戶列表
GET /getUserList
# 查詢用戶詳情
GET /getUserById
# 更新用戶信息
PUT /updateUser
# 刪除用戶
DELETE /deleteUser
URI 的設計規范
了解了 REST API 的一些基本概念,下面我們看下可以在實踐中應用的 URI設計規范。
根據 RFC 3986 - - Uniform Resource Identifier (URI): Generic Syntax 中的定義,一個 URI 的結構如下所示:
foo://example.com:8042/over/there?name=ferret#nose\_/ \______________/\_________/ \_________/\__/| | | | |
scheme(協議)authority(域名) path(路徑) query(查詢參數)fragment(片段)
我們這里主要針對 path 和 query 部分進行討論,對于 PATH 我們可以使用如下規范形式:
{domain}/{version}/{appid}/{resource}
{domain}/{version}/{appid}/{resource}/{sub-resource}/
{domain}/{version}/{appid}/{resource}/{action}
URI 主體字段含義
首先來看下 URL 中各個字段的含義與設計規范。
{domain}
表示 API 的域名。可以使用統一的域名,也可以針對不同的業務線使用不同的域名。{version}
表示 API 的版本。形式是 v + 數字,比如 v1, v2,有特殊需求是也可以進一步區分主版本和子版本,比如 v1.1, v1.2。一般只有在接口不兼容時才會升級。{appid}
服務的唯一標識。比如order
表示訂單服務,user
表示用戶服務,payment
表示支付服務。{resource}
具體的資源,要用名詞且為復數形式。比如orders
表示訂單資源,users
表示用戶資源,payments
表示支付資源。{sub-resource}
子資源,操作場景下和資源有依賴關系,要用名詞且為復數形式。比如購物車和購物車項。{action}
針對資源或子資源的行為操作,用動詞或者動詞短語表示,用來彌補 HTTP 方法表達上的不足。
URI 路徑規范
1. URI 中所有命名必須是小寫英文
下面是一個不規范的實例,使用了大寫字或非英文字母。
https://api.server.com/v1/訂單/orders
https://api.server.com/v1/PAYMENT/records
https://api.server.com/v1/order/orders/REFUND
2. URI 路徑分隔符推薦使用中劃線 -
下面是一個使用 _
的不規范實例
https://api.server.com/v1/marketing/coupons/get_by_code
3. URI 路徑中查詢參數命名必須統一使用 snake_case 或 camelCase 風格
在實踐中筆者通常傾向于統一使用 snake_case 風格,但有的團隊也會使用駝峰命名法。這里重要的是保持統一,避免在進行 API 設計時因為風格不一致導致不必要的溝通成本。
下面是一個使用 snake_case 風格的 URI:
https://api.server.com/v1/marketing/coupons/get-by-merchants?merchant_id=123456
下面是一個使用 camelCase 風格的 URI:
https://api.server.com/v1/marketing/coupons/getByMerchants?merchantId=123456
下面是一個使用駝峰命名法的不規范 URI,首字母用了大寫:
https://api.server.com/v1/marketing/coupons/get-by-merchants?MerchantId=123456
4. URI 中禁止出現 CURD 動詞,應該映射到 HTTP 方法
下面是一個不合法的 URI 示例,使用了 CURD 動詞:
GET /doctors/mjones/get-schedules
如果有特殊需求,應該有更明確的語義表達,但 Get、List、Update、Delete 這些 CURD 應該盡量避免。
5. URI 的路徑和參數必須以標準 UTF-8 編碼
比如出現漢字、空格、特殊符號等字符時,需要進行編碼。
6. URI 長度限制
RFC7230 中并沒有對 URI 的長度進行限制,但在實際開發中,最好限制在 2048 以內。如果 URI 過程,需要返回 HTTP 414 狀態碼(URI Too Long)。[參考 Stackoverflow]。
7. 資源 ID 規范
資源 ID 必須放到資源的后面,并且盡可能使用 UUID、HMAC 等類型的 ID,而不是數據庫的自增主鍵 ID,避免被人通過主鍵 ID 爬數據。比如
https://api.server.com/v1/payment/orders/298f37e-a538-11e9-93e8-0b39560ac73d
7. 子資源使用規范
只有在 API 操作場景下,子資源和資源有依賴關系時,才使用子資源。大致有兩類情況:
-
子資源不能獨立存在,必須依附于上級主資源訪問。比如某個用戶的簡歷信息,必須依附在某個用戶下。
-
子資源不能獨立表達含義。比如社交系統用的用戶和好友資源。可能最終訪問的是同一張數據表,但在用戶操作場景下可以獨立存在,而在好友場景下是依附于用戶存在的。
-
查詢用戶信息
# 查詢用戶詳情
GET .../users/1# 查詢用戶簡歷信息
GET .../users/1/resume# 查詢用戶好友
GET .../users/1/friends
在使用子資源時需要注意嵌套層級,盡可能不要使用超過 3 層的嵌套。
- 過多的嵌套會導致 API 過于復雜,不易理解
- 多級資源容易導致 URI 過長,引起一些兼容性問題。
8. 動詞使用規范
對資源的增刪改查應該使用標準的 HTTP 方法,比如 GET、POST、PUT、DELETE。下面是 HTTP 方法于操作的映射關系:
REST API 要求對資源的操作應該與 HTTP 方法對應,下面是資源操作的標準方法與映射關系。
資源操作 | HTTP 方法 | 描述 | 是否冪等 | 是否支持 Body | 響應格式 |
---|---|---|---|---|---|
List | GET | 用于查詢操作,對應數據庫的 select 操作 | ?? | ? | 資源列表,無數據時返回空列表 |
Get | GET | 用于查詢操作,對應數據庫的 select 操作 | ?? | ? | 資源詳情,無數據時返回 404 |
Update | PUT | 用于所有的信息更新,對應數據庫的 update 操作 | ?? | ?? | 資料詳情 |
Delete | DELETE | 用于更新操作,對應數據庫的 delete 操作 | ?? | ? | 空 |
Create | POST | 用于新增操作,對應數據庫的 insert 操作 | ? | ?? | 資源詳情 |
HEAD | 用于返回一個資源對象的“元數據”,或是用于探測API是否健康 | ?? | ? | 資源詳情 | |
UPDATE | PATCH | 用于局部信息的更新,對應于數據庫的 update 操作 | ? | ? | 資源詳情 |
OPTIONS | 獲取API的相關的信息。 | ?? | ? | 空 |
以下是基本的 API 示例
// 創建用戶
POST /users//查詢用戶列表
GET /users// 查詢用戶詳情
GET /users/1// 更新用戶信息
PUT /users/1// 刪除用戶
DELETE /users/1
如果有特殊的動作可以在路徑中使用 action 來標識,action 必須是動詞性質的單詞或短語。比如
# 實名認證
POST /users/1/real-name-auth# 取消訂單
PUT /orders/123456/cancel# 激活優惠券
PUT /coupons/123456/activate
常用標準字段
API 中通常有許多通用字段,比如名稱,排序,分頁,時間戳等,下面是一些常用的標準字段和相關規范。
字段名 | 類型 | 說明 |
---|---|---|
name | string | 資源名稱 |
parent | string | 父資源名稱 |
create_time | timestamp | 創建時間 |
create_by | string | 創建者 |
update_time | timestamp | 更新時間 |
update_by | string | 更新者 |
delete_time | timestamp | 刪除時間 |
delete_by | string | 刪除者 |
expire_time | timestamp | 過期時間 |
start_time | timestamp | 開始時間 |
end_time | timestamp | 結束時間 |
time_zone | string | 時區名稱, 取值應遵從 Time Zone Database 中給出的時區名稱 |
region_code | string | 地區編碼,取值應遵從 unicode_region_subtag 標準 |
language_code | string | 語言編碼,取值應遵從 Unicode_Language_and_Locale_Identifiers |
currency_code | string | 貨幣編碼,取值應遵從 ISO 4217 標準 |
mime_type | string | 媒體類型,取值應遵從 Mime 類型 標準 |
page_size | int32 | 分頁大小 |
page_number | int32 | 分頁頁碼 |
total_size | int32 | 數據總數 |
total_page | int32 | 數據總頁數 |
sort/order_by | string | 排序字段 |
asc/desc | string | 排序順序 |
filter | string | 過濾器參數 |
and、or、not | 過濾器表達式要支持的邏輯操作符 | |
=,!=,>,<,>=,<= | 過濾器表達式要支持的比較操作符,實際使用中有時也用單詞表示:eq,ne,gt,lt,ge,le 表示 | |
search/query | string | 搜索參數 |
sort_order | string | 排序順序,取值為 asc 或 desc |
create_by | string | 創建者 |
update_by | string | 更新者 |
status | string | 狀態 |
remark | string | 備注 |
HTTP 規范
Header 規范
應該盡可能使用標準的請求/響應頭。以下是一些常用的標準請求頭:
字段名 | 類型 | 說明 |
---|---|---|
Content-Type | string | 請求體格式,比如 application/json |
Accept | string | 告訴服務器可以接收的內容類型,如:application/xml, text/xml, application/json, text/javascript (for JSONP, 大多數時候都是選擇選擇application/json) |
Accept-Language | string | 告訴服務器可以接收的語言,如:zh-CN, en-US |
Accept-Encoding | string | 告訴服務器可以接收的編碼,如:gzip, deflate |
Accept-Charset | string | 告訴服務器可以接收的字符集,如:utf-8, utf-16 |
Authorization | string | 認證信息,取值為 Bearer token |
Date | string | 客戶端的時間戳,最好是 UTC 時間。但服務端不能依賴這個字段,因為客戶端的時間可能不準確。 |
緩存相關 Header
字段名 | 類型 | 說明 |
---|---|---|
Expires | string | 告知客戶端緩存過期時間,如果與 Cache-Control 同時出現,則以 Cache-Control 為準。 |
Cache-Control | string | 緩存控制 |
Last-Modified | string | 告知客戶端資源最后修改時間 |
If-Modified-Since | string | 將 Last-Modified 的值發送給服務器,服務器判斷資源是否被修改,如果被修改,則返回 200 狀態碼,否則返回 304(Not Modified) 狀態碼 |
ETag | string | 告知客戶端資源的唯一標識 |
If-None-Match | string | 將 ETag 的值發送給服務器,服務器判斷資源是否被修改,如果被修改,則返回 200 狀態碼,否則返回 304 狀態碼 |
同源策略相關 Header
字段名 | 類型 | 說明 |
---|---|---|
Access-Control-Allow-Origin | string | 告知客戶端可以訪問的源,如:* |
Access-Control-Allow-Methods | string | 告知客戶端可以訪問的方法,如:GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | string | 告知客戶端可以發送的請求頭,如果有自定義 header,則需要在這里聲明 |
Access-Control-Expose-Headers | string | 告知瀏覽器可以訪問的響應頭,默認情況下,只有 6 個基本字段可以被瀏覽器訪問:Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma |
Access-Control-Max-Age | integer | 指定預檢請求的結果可以緩存多久(以秒為單位)。 |
Access-Control-Allow-Credentials | string | 告知客戶端是否可以發送 cookie,如:true |
對于 POST、PUT、DELETE 等“高危”方法或者帶有自定義請求頭的方法,通常需要發送一個預檢請求(OPTIONS)進行檢查。
響應碼規范
必須使用正確的 HTTP 狀態碼。HTTP 協議定義的狀態碼分類如下:
狀態碼 | 分類 | 說明 |
---|---|---|
1xx | 信息性狀態碼 | 表示臨時響應,需要客戶端進一步操作 |
2xx | 成功狀態碼 | 表示請求成功 |
3xx | 重定向狀態碼 | 表示需要客戶端進一步操作 |
4xx | 客戶端錯誤狀態碼 | 表示客戶端請求錯誤,比如 400 錯誤請求,401 未認證,403 禁止訪問,404 未找到資源,405 方法不允許,429 請求過多 |
5xx | 服務器錯誤狀態碼 | 表示服務器處理請求錯誤,比如 500 服務器錯誤,502 網關錯誤,503 服務不可用,504 網關超時 |
- 在 API 設計開發時,至少需要區分 2xx、 4xx、5xx 三種狀態碼。
- 在必要時可以細化狀態碼
- 創建成功:201 Created
- 查詢成功:200 OK
- 更新成功:200 OK,或 204 No Content,表示執行成功但不返回數據
- 刪除成功:200 OK;未找到資源:404 Not Found,資源已被刪除或不可用 410
響應體規范
- 對于 List 操作,返回的是資源數組;如果沒有資源,則返回空數組。
- 對于 GET/POST/PUT,通常返回資源詳情對象
- 對于失敗的請求,除了 HTTP 狀態碼外,需要有更詳細的錯誤信息。下面是常用字段:
字段名 | 類型 | 是否必填 | 說明 |
---|---|---|---|
code | string | 是 | 業務自定義的錯誤碼 |
message | string | 是 | 用戶能讀懂的出錯信息 |
target | string | 否 | 出錯目標對象 |
details | Error[] | 否 | 錯誤列表 |
help | string | 否 | 幫助文檔地址 |
需要注意不要在 details 中異常調用棧信息,這個應該在服務日志中打印。
向后兼容規范
1. 不能減少現有參數
API 的修改或升級,必須是做加法,不能是減法。
2. 新增參數或請求數據必須有默認值
新增參數時,必須有默認值,不能是必選參數。否則會導致現有的客戶端調用失敗。
3. 不能修改原 API 的語義和簽名
已經發布的 API 有用戶在使用,因此任何對 API 的改動都不能對使用方造成負面影響。為此必須做到:
- 新增的查詢參數不能是必選的。
- HTTP 頭和狀態碼不能修改,可以增加,但必須有默認值。
- 請求數據中,必選參數不能有任何修改,包括參數名、類型、值范圍(可以擴大,但不能縮小)。
- 響應數據中,必選參數不能有任何修改,包括參數名、類型、值范圍(可以擴大,但不能縮小)。
如果做不到兼容,則需要升級 API 版本。
文檔規范
API 必須有文檔說明,最佳實踐是使用 Swagger 生成 API 文檔。內部應該有統一的 API 文檔管理平臺,對外提供 API 文檔的訪問。