後端工程師最基本的技能要求是設計符合 HTTP-based REST 的API,工作兩年多快三年,自己腦中第一反應大概會是
把URL視為資源路徑的描述,把對應的CRUD 操作對應至 HTTP Method,例如要下一筆訂單是
POST /booking
,取得單筆訂單是GET /booking/1
但世界沒有這麼單純,如果是遇到複雜的操作時,難以運用 HTTP 的 Method動詞 + URL 名詞的方式描述,那該怎麼辦呢?
例如說最近在工作上遇到如何設計一次刪除多筆資料的API該怎麼辦?
尋找答案的過程中,看到 Google 與 Microsoft 有公開他們的 API Design Guideline,並分享其中思考的眉角,其中包含了
- REST 的基礎觀念
- 應付複雜場景的考量
- 錯誤碼的處理
- 參數名稱與版本控制
API Design Guide | Cloud APIs | Google Cloud
以 Google 文件為主,Microsoft 文件為輔,整理過後分享個人筆記
一點 REST 介紹
Roy Fielding 在西元 2000年提出了 Representational State Transfer(REST ) 網站架構設計規範,同時他也是 URL / Http 1.0 / Http 1.1 標準制定的參與者,所以 REST 的概念很自然的與 HTTP 相當吻合,一開始 REST被誤以為是 HTTP object model
一種 HTTP的實作,但實際上 REST描繪的是一個正確架構的 Web 應用程式:用戶選擇連結(state transition),進而得到下一個結果(representing the next state of the application)
,其中提出了 REST 架構需要符合以下六大原則
Client / Server
依據關注點分離( Separation of Concern),將用戶介面跟資料儲存切割,方便兩者獨立運作與維護
Stateless
每一次的連線本身都攜帶足夠的資訊,而不用依賴於上一次連線的狀態,增加服務的可靠性與擴展性
Cache
根據 Request,Response 可以決定是否能被緩存,增加使用效率
Uniform Interfaces
如同程式設計,元件間也需要制定介面(interface)解耦合與溝通,雖然會降低一些效率,不過增加元件獨立的運作與維護,其中包含四個規範
- identification of resources:定位到特定資源上,資源可以是圖片、文字、特定服務(如今天加州天氣)、一個實體的人等等
- manipulation of resources through representations:Server 提供可以操作資源的方法,包含了描述資源本身的meta-data,以及如何操作 data的 Control data
- self-descriptive messages:訊息本身資訊量是足夠的,在跨元件之間可以不斷的被傳遞而不需要有額外的處理(Stateless)
- hypermedia as the engine of application state(HATEOAS):
模擬瀏覽網頁,加載完首頁後,後續操作都是利用網頁上的超連結探索網站;
套用同樣的邏輯至 REST Server,Response 包含針對此資源探索的連結與操作方式,如跟醫師預約後,回傳結果,同時包含查詢預約、查詢醫生資料、更改預約等操作都一併回傳
|
|
Layered System
一個完整的系統可以由多個子元件疊加,例如 Cache Server / Api Server / Proxy / Agent 等,每個元件僅可意識到相鄰的元件
Code On Demand
Client 可以依照需求加載新的 script 執行
這代表 REST 不一定要綁定 HTTP,只是 HTTP很巧妙的跟 REST概念十分貼近,REST的概念也可以對照到 HTTP 實作上(畢竟 Roy Fielding都有參與其中
回過頭來看常見的 RESTful API定義,比較像是 REST + HTTP 的混合產物,快速翻完 Roy Fielding 的 Architectural Styles and the Design of Network-based Software Architectures,其中的 6.2 URI 定義是特定資源的表徵 (representation of the identified resource)
,最一開始的 Web 是在傳輸文件或超連結(hypertext),所以 Resource 常直接對應到檔案文件,而 URI 則對應到實際檔案的路徑;這會帶來幾個不好的影響,例如說文件被修改了該如何表示 / 如何表達服務( Service )而非文件本身 等等;
良好的 Resource 定義應該是**盡可能固定不變** 且 抽象於實際檔案儲存本身,而是一種高層級的映射概念,當 Server 收到後去找出對應的實作內容,更重要的是傳達使用者的意圖,所以一個 Resource 概念可能橫跨多的檔案,也可以多個 Resource 描述同一個檔案
用 URI描述資源後,需要再加上用戶對於資源的操作(representation ),就可以組成語意 (Semantic),對應回 HTTP,不同的操作對應不同的方法( Method ),如 GET 表示要取得 URI所代表資源的資訊 / POST 表示創建 URI資源的子資源等,則定義在 HTTP 1.1 當中的 Method Definitions
以個人淺見,談到 REST Architecture 指的應該是符合 Roy Fielding 規範的 6大原則的網站架構設計;
而目前常用的 RESTful API 設計原則則是 REST中的 Resource 解釋加上 HTTP 對於 Method 操作的補充,兩者混合的設計原則
回歸 RESTful API / REST API
根據 Google 文件,RESTful API 定義主要是 可個別呼叫的「資源」(API 的「名詞」)「集合」做為模型。不同的資源有各自的參照名稱,也就是所謂的**[**資源名稱**](https://cloud.google.com/apis/design/resource_names?hl=zh-tw)**,並且是透過一套「方法」來操控
這樣的理念可以被很好的套用在 HTTP 1.1 上,因為 URL 可以用來描述資源路徑,HTTP Method 常用的也就是 GET / POST / PUT / PATCH / DELETE,正好對應資源的操作,根據 Google 文件,有 74%的公開 HTTP API 都是依據 REST 設計規範;
HTTP被廣泛應用,但也不是唯一的跨進程溝通的規範,如果是公司內網或是要求更低溝通上的overhead,會採用 RPC ( Google推廣自家的 gRPC),REST 也可以被套用在這上面
資源 Resource
面向資源導向設計的 API,需要先規劃資源的層級,每個資源的節點可以是單個資源(Resource)或是同一項資源的集合(Collection)
每個資源或集合必須有獨特的 id 去區分,在命名時盡可能表達清楚,避免使用以下的名詞如 resource / object / item 等;
且命名時已複數名詞表示,例如 /users/123/events/456
例如 Gmail API 會有一群用戶的集合,每個用戶底下有訊息的集合 / 標籤的集合等等
-- user
|— message
|— label
URL 的長度在Spec 中沒有規範,但在現實中有些瀏覽器會有長度限制,可以的話還是保持在 2000 字元以內比較保險
What is the maximum length of a URL in different browsers?
Microsoft 文件也是標榜類似的寫法,但額外建議不要讓資料的層級超過三層 /collection/item/collection
,例如說 /customers/1/orders/99/products
可以分拆成 /customers/1/orders
+ /orders/99/products
另外在 API的資源層級不要底層的資料結構有太緊密的關係,例如使用關聯性資料庫不一定要剛好對準 Table,API 應該是要更高層度的抽象化,避免之後資料庫的更動耦合了 API的資源路徑。
操作 Methods
面向資源導向設計的 API 側重於資源的描述而非操作的描述,所以大量資源的描述僅會搭配有限的操作,標準的操作是 List / Get / Create / Update / Delete 這五項
操作有可能不會是立即發生,需要一段時間才會生效,此時可以回一個長時操作的資源,用以查詢進度或是狀態等的方式,有點像是叫號取餐的遙控器
List
- 必須使用
GET
- Request 不能有 Body
- Response Body 包含以陣列表示的資源,與其餘選擇性的操作(如分頁操作)
GET
- 必須使用
GET
- Request 不能有 Body
- Response Body 對應到完整的資源描述
CREATE
- 必須使用
POST
- Request Body 必須包含新增資源的內容
- 如果支援 client side 指定
_id,則提供該欄位於 Request Body,但如果發生衝突需回傳 ALREADY_EXISTS
- Response Body 可以
Update
- 如果是部份更新使用
PATCH
- 如果是全部更新(覆蓋)使用
PUT
,如果 Request Body 沒有夾帶的欄位,視為清除該欄位 - 如果 Patch更新不存在資源,API 可以選擇是否支援 Upsert
更新不到就創建
的功能,如果否則回傳 NOT_FOUND - Response Body 必須是更新後的資源本身
- Update 僅用於更新,如果是其餘複雜操作如重新命名資源、改變資源路徑等,請使用客製化操作
如果以 JSON 為資料交換格式,Patch 有兩種方法 JSON patch / JSON merge patch,在資料儲存中,null
的含義有些模糊地帶,如果 Request JSON 欄位夾帶 null,這是代表移除該欄位
還是更新欄位成為 null
呢?
如果 Header 中的 Content-Type 是 application/merge-patch+json
則代表 null 移除該欄位,此時需注意資料結構就不建議欄位儲存 null避免混淆,更多參考 RFC: JSON Merge Patch;
如果希望針對欄位有更精確的操作描述,例如新增、刪除、取代、複製、搬移、驗證(test)等,可以參考 JSON patch,Content-Type 為 application/json-patch+json
,用陣列表述操作的集合,操作範例如下
|
|
DELETE
- 必須使用
DELETE
- 不能有 Request Body
- 如果是立即刪除,則 Response Body 為空值
- 如果是需要長時間執行,回傳相對應的長時操作
- 如果只是把資源標記刪除而非硬刪除,回傳更新的資源
- 刪除必須是冪等性操作,不論操作幾次都必是是刪除該資源,但回應內容可以改變,第一次確實刪掉資源回復成功,後續刪除可以回覆 NOT_FOUND
Custom Method
除了上述五種操作外,可能會有些操作不再這五種範疇之中,此時就可以自行定義,例如說取消刪除、大量更新等
自定義的方法須以 :
放在 URL的最後方,且常見的搭配是用 POST
,因為可以夾帶Body;
但也可以視情況使用其他的 HTTP Method (Patch 不建議使用外),但仍須遵守 HTTP Method 使用的規範,例如冪等性與是否能夾帶 Body等
Google 提供幾種自定義方法的用途
1. POST :cancel 取消操作
2. GET :batchGet 一次性取得多筆資料
3. POST :move 將資源移到別處
batchGet 也可以使用 POST,例如 POST https://mybusiness.googleapis.com/v4/{name=accounts/*}/locations:batchGet` 在 Body 中有對查詢的內容有更近一步的描述
Microsoft 提議就把非名詞放進 URL中,例如一個計算機的加法 API可以設計為 GET /add?operand1=99&operand2=1
,其餘的就遵守 HTTP Method 操作方式。
在 AWS 文件中,Custom Method 是以 Query String 方式存在,如 刪除多筆物件/?delete
,個人是覺得容易與其他的 Query String 混淆,在 Parsing上也比較麻煩點,不是很喜歡這樣的作法
個人偏好 Google的做法,讓 URL組成全部都是名詞,乾淨的表示資料層級,Custom Method 就以一個特殊的方式宣告,可以良好與 REST API共存
HTTP Media Type 與 Header
最後別忘了 Request 與 Response 應盡量遵守 HTTP規範,使用正確的 Status Code 與 Header,讓 API設計更精確,例如
- 交換的資料格式為 json 就要宣告
Content-Type: application/json;charset=utf-8
- 200
201(Created) : 建立新資源
202(Accepted):接受請求,但不是立即發生
204 (No Content):沒有要回傳資料
Versioning
在維護 API過程,會遇到更動資料結構或是修改邏輯的地方,如果可以的話又不希望影響原有 API的操作,此時就需要做版本控制,版號的命名可以參考 Semantic Version X.Y.Z 方式表示
- X 大版號表示有 breaking change,向前不相容
- Y 小版號表示有新增 function,或是更動是向前相容的
- Z 補丁版號表示 Bug 修正
一般來說 API 對外用大版號表示,如 v1 / v2,小版號跟補丁版號出現在文件的版本號上
version 常見可以放在幾個地放
- URL 當中,放在 Domain 後的第一層,如
https://example.com/v1
- 夾帶在 Query String中,如 https://example.com?version=v1
- 放在 Custom Header當中
- 放在 Header Accept 中,如
application/vnd.adventure-works.v1+json
在採用 versioning 時,要考量到 web cache 的機制,通常 cache 是針對 URL,所以前兩者比較推薦
Error
常見作法會回傳錯誤代碼、錯誤的簡述、錯誤的詳細內容,方便開發者做錯誤處理,例如以下格式
|
|
Code 常見還可以用數字表示,方便開發者透過文件快速查詢,但也不要忘了回傳該次錯誤的內容描述
別忘了要搭配正確的 HTTP 錯誤碼使用
400(Bad Request):Server無法理解,例如參數缺少或錯誤
401(Unauthorized):授權錯誤,可能是 Token失效等
403(Forbidden):無訪問權限,想取得超過用戶授權的資源
404(Not Found):查無資源
…..