如何設計 REST API

後端工程師最基本的技能要求是設計符合 HTTP-based REST 的API,對你來說 REST API 的第一印象又是什麼呢? 面對一些特殊狀況沒辦法很好用現有的 REST 表達,你又會如何設計呢?

後端工程師最基本的技能要求是設計符合 HTTP-based REST 的API,工作兩年多快三年,自己腦中第一反應大概會是

把URL視為資源路徑的描述,把對應的CRUD 操作對應至 HTTP Method,例如要下一筆訂單是 POST /booking ,取得單筆訂單是 GET /booking/1

但世界沒有這麼單純,如果是遇到複雜的操作時,難以運用 HTTP 的 Method動詞 + URL 名詞的方式描述,那該怎麼辦呢?

例如說最近在工作上遇到如何設計一次刪除多筆資料的API該怎麼辦?

尋找答案的過程中,看到 Google 與 Microsoft 有公開他們的 API Design Guideline,並分享其中思考的眉角,其中包含了

  1. REST 的基礎觀念
  2. 應付複雜場景的考量
  3. 錯誤碼的處理
  4. 參數名稱與版本控制

API Design Guide | Cloud APIs | Google Cloud

microsoft/api-guidelines

以 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)解耦合與溝通,雖然會降低一些效率,不過增加元件獨立的運作與維護,其中包含四個規範

  1. identification of resources:定位到特定資源上,資源可以是圖片、文字、特定服務(如今天加州天氣)、一個實體的人等等
  2. manipulation of resources through representations:Server 提供可以操作資源的方法,包含了描述資源本身的meta-data,以及如何操作 data的 Control data
  3. self-descriptive messages:訊息本身資訊量是足夠的,在跨元件之間可以不斷的被傳遞而不需要有額外的處理(Stateless)
  4. hypermedia as the engine of application state(HATEOAS)
    模擬瀏覽網頁,加載完首頁後,後續操作都是利用網頁上的超連結探索網站;
    套用同樣的邏輯至 REST Server,Response 包含針對此資源探索的連結與操作方式,如跟醫師預約後,回傳結果,同時包含查詢預約、查詢醫生資料、更改預約等操作都一併回傳
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<appointment>  
  <slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>  
  <patient id = "jsmith"/>  
  <link rel = "/linkrels/appointment/cancel"  
        uri = "/slots/1234/appointment"/>  
  <link rel = "/linkrels/appointment/addTest"  
        uri = "/slots/1234/appointment/tests"/>  
  <link rel = "self"  
        uri = "/slots/1234/appointment"/>  
  <link rel = "/linkrels/appointment/changeTime"  
        uri = "/doctors/mjones/slots?date=20100104&status=open"/>  
  <link rel = "/linkrels/appointment/updateContactInfo"  
        uri = "/patients/jsmith/contactInfo"/>  
  <link rel = "/linkrels/help"  
        uri = "/help/appointment"/>  
</appointment>

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 ,用陣列表述操作的集合,操作範例如下

1
2
3
4
5
6
7
8
[  
     { "op": "test", "path": "/a/b/c", "value": "foo" },  
     { "op": "remove", "path": "/a/b/c" },  
     { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },  
     { "op": "replace", "path": "/a/b/c", "value": 42 },  
     { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },  
     { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }  
]

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設計更精確,例如

  1. 交換的資料格式為 json 就要宣告 Content-Type: application/json;charset=utf-8
  2. 200
    201(Created) : 建立新資源
    202(Accepted):接受請求,但不是立即發生
    204 (No Content):沒有要回傳資料

Versioning

在維護 API過程,會遇到更動資料結構或是修改邏輯的地方,如果可以的話又不希望影響原有 API的操作,此時就需要做版本控制,版號的命名可以參考 Semantic Version X.Y.Z 方式表示

  1. X 大版號表示有 breaking change,向前不相容
  2. Y 小版號表示有新增 function,或是更動是向前相容的
  3. Z 補丁版號表示 Bug 修正

一般來說 API 對外用大版號表示,如 v1 / v2,小版號跟補丁版號出現在文件的版本號上

version 常見可以放在幾個地放

  1. URL 當中,放在 Domain 後的第一層,如 https://example.com/v1
  2. 夾帶在 Query String中,如 https://example.com?version=v1
  3. 放在 Custom Header當中
  4. 放在 Header Accept 中,如 application/vnd.adventure-works.v1+json

在採用 versioning 時,要考量到 web cache 的機制,通常 cache 是針對 URL,所以前兩者比較推薦

Error

常見作法會回傳錯誤代碼、錯誤的簡述、錯誤的詳細內容,方便開發者做錯誤處理,例如以下格式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{  
  "error": {  
    "code": "BadArgument",  
    "message": "Multiple errors in ContactInfo data",  
    "target": "ContactInfo",  
    "details": [  
      {  
        "code": "NullValue",  
        "target": "PhoneNumber",  
        "message": "Phone number must not be null"  
      },  
      ....  
    ]  
  }  
}

Code 常見還可以用數字表示,方便開發者透過文件快速查詢,但也不要忘了回傳該次錯誤的內容描述

別忘了要搭配正確的 HTTP 錯誤碼使用
400(Bad Request):Server無法理解,例如參數缺少或錯誤
401(Unauthorized):授權錯誤,可能是 Token失效等
403(Forbidden):無訪問權限,想取得超過用戶授權的資源
404(Not Found):查無資源
…..

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus