為了理解目前 API 設計的主流,試著整理了網路上的解說文章,作為學習筆記。


何謂 RESTful?

簡單來說,就是把每個網址當做資源(Resource)來看待,對同一個資源做不同的動作(HTTP Verb)會得到不同的結果。
符合 REST 概念設計的網址,又稱之 RESTful Route。


  • REST 全名

    • Resource : 資源
    • Representational : JSON, XML, YAML..
    • State Transfer : 狀態傳輸,透過 HTTP 動詞實現
  • API 範例

    • 非 RESTful 的 API 可能長這樣:
    1
    2
    3
    4
    /api/get_file/ ( 得到檔案 )
    /api/upload_file/ ( 新增檔案 )
    /api/update_file/ ( 更新檔案 )
    /api/delete_file/ ( 刪除檔案 )
    • RESTful API :
    1
    2
    3
    4
    /api/files/ ( GET -> 得到檔案 )
    /api/files/ ( POST -> 新增檔案 )
    /api/files/ ( PUT -> 更新檔案)
    /api/files/ ( DELETE -> 刪除檔案 )

RESTful Triangle

相關圖片

  1. Nouns 名詞:定義網址的 URL,每個資源僅有一個唯一的識別位置,就像家的住址。
  2. Verbs 動詞:描述了對 Nouns 名詞 (資源 URL) 的操作動作,在 HTTP 1.1 的實作就是 HTTP Method。
  3. Content Types 資源呈現方式:比如取得某一個 URL 文章的 HTML 格式、或者 XML 格式,同樣的 URL 資源可以有不同型態的表現方式。

建議在設計 API 規格時,同時思考上述的設計原則,好讓 API 設計更接近 REST 精神。




RESTful API 設計要點


HTTP 動詞

HTTP*通訊協定中制定了九種動詞(Verbs)來跟伺服器溝通,分別是HEAD*、_GET_、_POST_、_PUT_、_PATCH_、_DELETE_、_TRACE_、_OPTIONS_、_CONNECT_。

  • 常用的 HTTP method:
HTTP Method Idempotent Safe
GET(讀取資源) yes yes
HEAD(類似 GET,但只回傳 HTTP header) yes yes
PUT (替換資源) yes no
POST(新增資源) no no
DELETE(刪除資源) yes no
PATCH(更新資源部份內容) no no
  • Safe / Idempotent

    • 「safe」是指該操作不會改變伺服器端的資源狀態(而且結果可以被 cache)

    • 「idempotent」是指該操作不管做 1 遍或做 n 遍,都會得到同樣的資源狀態結果(但不一定得到同樣的回傳值,例如第 2 次 DELETE 請求可能回傳 404),因此 client 端可以放心 retry。

  • PUT、POST 和 PATCH 容易混淆,補充說明如下:

    1. PUT通常是用來替換單一資源或資源集合 (resource collection) 的內容。
    2. POST除了用來新增資源,也作為 catch-all 用途,例如用在 utility API。(Utility API 是不同於一般資源讀寫操作的要求類型,例如檢查某個促銷活動碼是否有效。)
    3. PATCH用來更新資源部份內容。前幾年有人會用 POST 代替 PATCH,現在應該沒這必要了;建議除非 infrastructure 有限制,否則直接用 PATCH 即可。

URI 名詞

相對於 HTTP 動詞,URI 就是名詞了。URI 由 prefix + API endpoint 組成。Prefix 的部份可有可無,例如/api/api/v1。API endpoint 的設計,幾個重要原則如下:

  • 一般資源用複數名詞,例如 /books/books/123
    • 有些人認為用單數比較好,因為/book/123看似比/books/123合理;但想想檔案系統的目錄命名(例如/Users/Documents),其實用複數也沒問題。複數可以保持 API endpoint 的一致性,所以一般資源建議用複數。
  • 唯一資源(亦即對 client 而言只有一份的資源)用單數名詞。例如GitHub watching API中的GET /user/subscriptions,其中user是指目前驗證的使用者,所以用單數。
  • 資源的層級架構,可以適當反應在 API endpoint 設計上,例如/books/123/chapters/2
  • Utility API 與 resource API 性質不同,它的 endpoint 設計只要合理即可,例如/search?q={keywords}
  • 建議 URI components 都用小寫,兩個字之間用減號-或底線_隔開皆可,但應保持一致。(我個人偏好用-

HTTP 回傳狀態碼

API 回傳的結果,應使用適當的 HTTP 狀態碼,所以 API 設計者必須了解它們。以下是一些常用的狀態碼,完整列表請參考Wikipedia

  • 2xx: 成功

    • 200 OK: 通用狀態碼
    • 201 Created: 資源新增成功
    • 202 Accepted: 請求已接受,但尚在處理中
    • 204 No Content: 請求成功,但未回傳任何內容
  • 3xx: 重新導向

    • 301 Moved Permanently: 資源已移至它處
    • 303 See Other: 回傳的內容可在它處取得(例如在用戶端發送了一個 POST 請求之後)
    • 304 Not Modified: 請求的資源並未修改(通常是用戶端發送了帶有 If-Modified-Since 或 If-None-Match 表頭的請求)
  • 4xx: 用戶端錯誤

    (用戶端不應 retry 原始請求)

    • 400 Bad Request: 通用狀態碼
    • 401 Unauthorized: 用戶端尚未驗證*
    • 403 Forbidden: 用戶端被禁止此請求*
    • 404 Not Found: 請求的資源不存在
    • 405 Method Not Allowed: 不支援請求的 HTTP 方法
    • 406 Not Acceptable: 不支援請求所要求的內容類型*(Accept 表頭)
    • 415 Unsupported Media Type: 不支援請求所的內容類型*(Content-Type 表頭)
  • 5xx: 伺服器錯誤

    (用戶端可合理 retry)

    • 500 Internal Server Error: 工程師要找 bug 了
    • 501 Not Implemented: 用戶端的請求目前未支援(也就是將來有可能支援)
    • 502 Bad Gateway: 上游的伺服器未回傳正確結果,一般是 gateway 或 proxy server 才會回傳此狀態碼
    • 503 Service Unavailable: 暫停服務(也就是過不久就會恢復服務 ── 如果一切順利的話)
    • 504 Gateway Timeout: 上游的伺服器逾時,一般是 gateway 或 proxy server 才會回傳此狀態碼

* 關於幾個容易混淆的狀態碼,補充說明如下:

  1. 401、403: 401 是指用戶端尚未驗證,也就是 unauthenticated(HTTP spec 裡用 unauthorized 有些誤導);403 是指用戶端目前的身份不被允許此項請求(通常是用戶端已驗證過了),或是所有使用者都不被允許此項請求。
  2. 406、415: 406 是指用戶端要求「回傳」的 Content-Type(也就是用戶端在 Accept 表頭裡所要求的),伺服器不支援;415 是指用戶端送出的「請求」,其 Content-Type(也就是用戶端 HTTP request body 的內容類型),伺服器不支援。

另外要注意,這些回傳的狀態碼,是代表 API 這一層的執行狀態,而不是商業邏輯這一層的狀態。例如當/search?q=xyz搜尋結果是空的,API 結果仍應回傳 200,而非 404;因為從 API 角度來看,/search這個「資源」存在,而且 API 執行成功。


HTTP Header

用戶端送出 API 請求時,可能會帶一些 HTTP header,例如:

  • Accept: 能夠接受的回應內容類型 (Content-Type),屬於內容協商的一環
  • Authorization: 認證資訊

至於 API 回傳結果的 HTTP header,沒甚麼特別之處,按照一般原則處理即可(例如 Content-Type、Content-Length、ETag、Cache-Control…)。


HTTP Body: JSON 或 XML 格式

現在 JSON 已被普遍支援,加上 JSON 處理上較簡潔,所以越來越多人採用 JSON 作為 API 的 HTTP body 格式。但要採用 JSON 或 XML(或同時支援兩種格式),仍應視專案的實際需求而定。


其它原則

  1. 與 HTTP 一樣,API 應該是 stateless,也就是一項工作單元不應由二個或二個以上 API 組成。(這也引申出一個有趣的問題:REST API 如何支援 transaction?這個問題超出本篇文章的範圍,以後有機會再談。)
  2. REST API 所呈現的資源,是從應用面及 client 角度來思考,並不需要和後端的資源儲存形式(例如資料庫 schema)維持一對一的關係。
  3. HATEOAS (Hypermedia as the engine of application state) 雖然是 REST 原始定義裡的一環,但我認為不一定需要。
  4. Query parameter 的部份,只要風格保持一致即可,REST 對此並無特殊規範。



RESTful 架構的實踐


HTTP

HTTP 1.1 本身就是 REST 架構的實踐,我們可以遵循它定義好的規格,例如:

  • 透過 HTTP Status Code 回傳 API 執行結果
  • 利用 HTTP Accept Header 告知 Server 指定回傳的資料格式 Content-type,JSON 可以送出 application/json、XML 可以送出 application/xml 等等
  • 透過 POST 新增資料到 Server,執行成功後回傳 201 Status Code 並且透過 Location Header 告知新資源的 URL

優缺點

  • 優點:
    • 網址更直觀,不同開發人員都遵照慣例設計網址,易於維護、擴展。
    • REST 設計將每一個實體定義獨立唯一的 URL 位置,物件資源相互的依賴性降低,API 解耦特性更好,容易彈性組合應付多數業務邏輯。
  • 缺點
    • 安全性問題,假設使用者知道 /member/1 是自己的會員資料,他就可以嘗試從 /member/100 得到其他用戶的資料,須對使用者做身份驗證。
    • 資源相依結構鬆散問題,假設想要取得文章與作者資訊,就必須先 GET /文章/{ID} ,然後再透過取回的文章物件中的 Author 欄位再一次呼叫 GET /使用者/{NAME} 取得作者資訊,較為麻煩。


參考資料:

https://railsbook.tw/chapters/11-routes.html#restful-routes

https://ihower.tw/rails/restful.html

https://tw.twincl.com/programming/*641y

https://github.com/twtrubiks/django-rest-framework-tutorial/tree/master/RESTful-API-Tutorial

https://www.webguide.nat.gov.tw/News_Content.aspx?n=531&s=2918