RFC 裡面的 RESTful API

RESTful API 設計

API 是一種對伺服器資源的操作介面, 取得 "某項資源", 取得 "訂單資料", etc.

「API 設計」的理念, 我會朝以下方向去思考抉擇與設計

  1. 好的開發體驗, API 經常在跨團隊合作中被視為溝通的介面, 好的開發體驗對維護團隊或是對使用 API 的團隊來說, 都是很重要的

    • 對呼叫端 (Client) 的開發者來說, 是不是可以容易理解, 盡可能的操作少量的 API 就達到目的?

    • 對維護端 (Maintainer) 的開發者來說, 是否可以套用差不多的 pattern 快速完成開發與維護?

    • 這些 APIs 都應該被設計成有相同的操作概念.

  2. 盡可能朝向 RFC7231 標準靠近, 在跨程式語言的開發合作上, 若有爭議也有個參照標準可以參考.

  3. API 不等同於對資料庫的操作介面, 因此在設計上 不應該 以操作資料庫 Entity 的角度去思考.

  4. 效能, 負載考量的取捨

    • 在交易類型的 API 設計, 常常會需要把一大包的關聯資料設計為一次交易操作, 若要避免複雜的 JPA 關聯設計, 或許可以犧牲一些操作體驗, 來交換少一點的關聯.

    • 季報表, 年報表, 大量型的資料操作, 盡可能讓需求走向非同步. 就像在備份 Gmail 資料那樣, Google 會在完成備份後發 Email 給你告知備份已完成. 這樣可以減少一些 long time connections.

GET

GET 冪等的操作, 通常用來取得資料, 同樣的 URI 應該要取得同樣的資源, 我們會這樣設計:

# 由 ID 取得單筆資料
GET /claim/123
# 取得多筆集合資料
GET /claim/case?caseNo=aaa&status=bbb

取得資料成功 client 應該收到 http 200.

取得資料失敗對於由 ID 取得單筆資料的, 應該收到 http 404 not found, 因為該 URI 並沒有對應的資源.

然而取得多筆集合資料的, 並非由 ID 明確去定義資源, 而是由 ?caseNo=aaa 去過濾篩選, 因此一樣會回傳 http 200 的空集合 (空陣列) 的資料.

POST

POST 非冪等 (非冪等的意思是, 相同的資料新增了兩次, 也不會是同 1 筆資料) 的操作, 通常用於新增資源, 然而若查詢的條件超過 http URL 的長度限制, 也能考慮使用 POST.

對於伺服器來說, 新增資料成功應該回傳 http 201 描述資源已建立, 並且回傳 http header Location 告知 client 資源建立成功後要重新導向到對應 URI.

the origin server SHOULD send a 201 (Created) response containing a Location header field that provides an identifier for the primary resource created (Section 7.1.2) and a representation that describes the status of the request while referring to the new resource(s)

但有時候考慮到瀏覽器會因為 http Location header 而重新定向的影響, 有時我會折衷設計為當資源新增成功後, 直接回傳該資源的資料, 方便 client 操作, 但保留討論空間.

PUT

PUT 冪等的操作, 更新什麼就應該得到什麼, 我們通常會設計為指定 URI 資源 ID 去做更新, 通常會像是:

# 更新 clmCase ID 為 1 的資源
PUT /claim/case/1
{
  "caseNo": "aaa",
  "status": "ACTIVE",
  "accountingDateTime": "2024-01-01T00:00:00"
  ...
}

描述請求的 payload 若原本的資源欄位有 accountingDateTime, 而在 PUT 的操作 payload 沒有 accountingDateTime 欄位, 在 Java 的世界裡可能會有兩種狀況 accountingDateTime = null 或是 accountingDateTime = '' , 前者會被判別位會視為 client 要清除掉 accountingDateTime 的資料, 後者會因為要把字串類型的空字串 serialize 為 LocalDateTime 而導致 Server Error 500, 這在設計前需要先跟 Client 溝通好.

當使用 PUT 更新一筆不存在的資源時, 在 RFC 規範中應該協助 client 重新定向 302, 301 之類的, 但站在 Client 使用的立場, 3XX 反而會增加一些開發上的困擾, 或許這更像一次錯誤的業務操, 直接反應 404 not found 是我比較偏好的做法.

當然在 client 對資料操作的過程中, 有可能是同時對同 1 筆資料操作 (parallel 操作), 這可能會導致更新結果不如預期 (通常我們會設計上加上樂觀鎖來避免這件事), 而發生資料衝突, 將回傳 http 409 Conflict.

批次型作業的 PUT, 有些時候可能要設計為一整包的資料更新 (包含新增 element, 刪除 element, 修該 element), RFC2616 的 PATCH 可能比較能明確的區別 PUT 與 PATCH, 因為 新增, 刪除, 修改 已經跳脫了密等性, RFC2616 的規範會更符合這個情境. 若批次型作業 PUT 只會對 修改 這個行為反應, 那我覺得維持 PUT 是比較合適的. 一樣在設計前也需要特別跟 client 溝通好.

DELETE

DELETE 操作 (資源被刪除後就不能刪除第 2 次, 所以比較接近冪等操作), 通常用於刪除資源, 我們通常會這樣設計:

DELETE /claim/case/1

刪除成功回傳 http 204 no content, 若刪除的資料不存在 (在業務上或許是一種嚴重的錯誤, 畢竟 client 指定了不存在的資源做刪除), 因此我習慣回應 404 not found 做警示, 然而這也保留討論空間, 依據 RFC 規範 http 200 也是可以的, 但我傾向於, 相對於成功操作 http 200做一點區別, 故會以回傳 204 表示資料已刪除.

PATCH

PATCH 非冪等的操作, 依據 RFC2616, PATCH 不安全也不冪等, 舉例來說: 當有兩個 PATCH 同時間請求而發生碰撞時, 一個刪除資料, 一個新增資料, 會無法預期資料會發生什麼事 (資料衝突與損壞), 因此若要設計 PATCH 的 API, 應該明確定義好操作邊界.

通常來說 PATCH 的資料格式大概會像是

[
  { "op": "replace", "path": "/title", "value": "新標題" },
  { "op": "add", "path": "/tags", "value": "新標籤" },
  { "op": "remove", "path": "/comments/2" }
]

根據 RFC 的說明, 伺服器必須以原子操作方式 PATCH 所有變更, 不能只有其中一個操作成功, 而是要一起成功或一起失敗, 若第二個操作失敗, 則需 rollback 其他操作, 因此 PATCH 的 API 設計上會是較為複雜的. 然而在只針對部分欄位的補充/修改上, 相對於 PUT 方法會是更有效率的.

在畫面操作中, 有可能會等待 user 將所有 table cell 的資料都操作完成, 這些操作中可能包含移除某個項目, 又新增了其他項目, 也包含欄位編輯, 而這些操作要在同一個 "儲存" 按鈕下完成, 這對 user 來說可能是相當友善方便操作的介面, 但在資料狀態的角度, 卻不是很容易處理.

PATCH 是容易引發非常複雜的設計與流程, 我的權衡是開發與維護優先, 因此諸如此類的需求, 以畫面的 Grid table 應該在每個 table row 添加 editable 或 delete 的操作, 讓資料轉為單筆的交易操作, 降低業務開發的複雜度.

https://mdbootstrap.com/docs/standard/plugins/table-editor/