現代 Web 開發中,API 是提供資料存取的重要途徑之一。API 是開發人員的使用者介面,提供設計良好方便使用的 API,會讓開發人員樂於使用。

關於設計 API 的最佳方法,並沒有統一的答案,但一些基本需求和功能要素大致上是一致的。在進行實際系統的開發時,了解這些元素並在系統中適當的提供對 API 的品質很有幫助,以下是一些整理。

網址樣式及功能

網址中應具備 api 以明確表示此網址提供的是 API 服務,有 2 種型式:

  • 於網域名稱中: api.mydomain.com/…
  • 於應用程式名中: www.mydomain.com/api/…

當以 資源 / 資料 作為處理對象時,要以 複數名詞 (如: products) 來描述該資源,並用HTTP requests 的 method (GET, POST, PUT, PATCH, DELETE) 來提供不同功能的識別。

通用樣式如下:
{HTTP Verbs} {網域}/{應用程式:可選}/{資源識別名}/{項目識別碼:可選}

HTTP Verbs 提供的功能:

  • GET:讀取
  • POST:新增
  • PUT:更新 - 完整更新,沒值的欄位就替換成沒值
  • PATCH:部份更新 – 沒值的欄位不做更新
  • DELETE:刪除

如:

  • GET /products - 讀取全部 products
  • GET /products/12 -讀取一筆指定 product
  • POST /products – 新增一筆 product
  • PUT /products/12 – 全部更新 product #12
  • PATCH /products/12 – 部份更新 product #12 的一些欄位值
  • DELETE /products/12 – 刪除 product #12

相關資料(子資源)的存取

要處理的對象資源是另一個資源的子項目,那就在在母資源的後面接上該資源的描述名稱。如,要對 product 12 的 留言 做處理:

  • GET /products/12/messages
  • GET /products/12/messages/5
  • POST /products/12/messages
  • PUT /products/12/messages/5
  • PATCH /products/12/messages/5
  • DELETE /products/12/messages/5

不是做 CRUD 的操作時

視需求,有幾個作法:

  • 把操作結果轉為資源的一個資料欄位,特別是不需要額外參數的情況下。如,啟用(activate) Member 的動作,可以對應到 Member 叫 activated 的 Boolean 欄位,並用 PATCH 去異動它。
  • 把操作對應到一個子資源去描述,
    如要收藏一個商品,可用: PUT /products/{id}/ favorite
    取消收藏,可用: DELETE /products/{id}/ favorite
  • 如果真的沒有可轉化的描述方式,比如搜尋,那就用 /search 吧,至少它很明確不是一個資源而是操作。配合上文件,能讓用戶清楚了解這個 API 的目的是最重要的。

只用 SSL 連線

API 是對外開放的,基於內容安全性,只能接受 SSL 連線,除了保護內容外,更可減少每次呼叫都要做認證授權的工。

對於非 SSL 的連線,不要自動重導為 SSL,只丟錯誤讓用戶端自己處理即可。

IIS 定義 HTTP 403.4 明確指示需要 SSL,但它不是一個公用標準,還是可以用回覆 HTTP 403 做為需要 SSL 的代表回應。

要有良好的文件

每個 API 應該都要有良好的說明,最好是有完整的 request/response 內容範例。API 的說明文件可以利用工具程式來產生,以提供最即時的資訊。

說明文件必須公開於網路上,讓用戶容易找到並參考,避免只以文件檔的型式發布或是登入才能取得。

一旦發佈了一個公共 API,就是承諾不會在沒有通知的情况下做重大異動。文件必須包含停用時間表和 API 的更新說明或預告。更新資訊可以公開於網站 (如:部落格) 或以郵寄清單發布。

要有版本資訊

API 必須提供版本資訊,除了便於控制 API 的調整改變外,對於大改版後新舊並行的過渡期服務也很有幫助。

建議作法是網址和 Header 並行:
在網址提供主版本的識別號 (如:v1),並利用 Header 傳送次版本號。主版本號確保功能的一致性,副版本號則提供異動的識別。

1
2
3
https://api.shop.com/v1/products
Header >> "Api-Version: 2017-06-05"
Header >> "Api-Version: 1.0.2.168

如果不是公開服務或受影響用戶不多 (如:只內部使用或程式限定),或許第 1 版時,網址 v1 主版號可省略,真有改版再加上,並維持沒版本號的為最新版本,如:

1
2
https://api.shop.com/products
https://api.shop.com/v1/products

只用 JSON 格式

相對於 XML,JSON 有更好的可讀性、程式語言支援及較少的資料量。

Google 搜尋熱度的趨勢變化

搜尋熱度的趨勢

然而,如果客戶群由大量的企業客戶組成,那麼可能會不得不支持 XML。在支持 JSON 及 XML 雙格式的情況下,可利用 Accept Headers 或 URL 的方式指定。如果有平台支援,選擇平台支援度高的,不然以 URL 的方式做指定有較高的瀏覽器一致性。
如,可加上 .json 或 .xml:

1
https://api.twitter.com/1.1/search/tweets.json

欄位名稱用 snake_case

如果要依照程式語言的命名慣例,C# 和 Java及 JavaScript是用 camelCase, python 和 ruby 是用 snake_case。

但做為中性的資料格式, snake_case 有 較好的可讀性,很多公開的 API 也都採用了 snake_case。在套件能支援的情況下,優先採用 snake_case 格式。

更新或新增:必須返回異動資訊

為避免 API 用戶不清楚執行結果重覆呼叫,API 應該回覆清楚資訊。

  • 回 HTTP 201 status code
  • 在 response header 包含Location header 指到新建立的資源,如:
    1
    Location: https://api.shop.com/v1/products/123

預設回傳有格式的結果(Pretty print),確保有gzip 的支援

雖然做 white-space 清除(compressed)可減少資料量,但直接回傳有格式的結果有助於客戶端進行開發及除錯工作,相對於減少的傳輸量,有格式的結果更有助於 API 的品質。

如果有大資料量的壓縮的需求,可提供 gzip 的方案,對資料減量有更好的成效。

POST, PUT & PATCH 異動時,用 JSON encoded

簡單的 API 用 URL encoding 是足夠的,輸入內容複雜(如:物件) 的 API 應使用 JSON encoded 對 body 做傳送。

支援 JSON encoded POST, PUT & PATCH requests 的 API 要合於以下規範:

  • Content-Type header 要為 application/json
  • 不合就丟回 415 Unsupported Media Type HTTP status code

結果處理 - filtering, sorting & searching

可以視用戶需要,提供結果處理的功能。
主網址儘量保持簡潔,額外的處理要求,利用參數方式來指定。

Filtering:

利用特定的參數來提供,如:
GET /products?state=open

Sorting:

統一用參數 sort,如:
GET /products?sort=-priority,created_at
“-“ 號表示為 descending

Searching:

更進階的資料處理,可接受複合條件,如:
GET /products?q=return&state=open&sort=-priority,created_at

對於一般常用的查詢條件,可以提供別名的方式,如:
GET /products/recently_closed

限定回傳的欄位

API 的用戶未必都需要全部欄位的資料,如果可以提供指定欄位的功能,可以減少資料量增加效率。

統一用參數 fields 配合 comma “,” 來做欄位的指定,如:
GET /products?fields=pid_num,pid_name,updated_at

分頁

一般是利用 URL 接收需求,依取資料的方式不同有不同參數,如:page | page_size、offset | limit、count | max_id,像:

1
page=1&page_size=1000

也有利用 Header 傳分頁參數的方式。

回傳分頁資訊的方式,也有多種方式

一)可利用 Link header (RFC 5988) 這個規範,特別是游標型分頁。

如,GitHub:

1
2
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
<https://api.github.com/user/repos?page=50&per_page=100>;rel="last"

二)自訂的 Header,像是:
X-Pagination-TotalRecordCount: 1500
X-Pagination-PageCount: 150
X-Pagination-PageSize: 10
X-Pagination-PageNo: 5

三)直接加在回應內容中 (JSON Envelope),像 Facebook:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"data": [
... Endpoint data is here
],
"paging": {
"cursors": {
"after": "MTAxNTExOTQ1MjAwNzI5NDE=",
"before": "NDMyNzQyODI3OTQw"
},
"previous": "https://graph.facebook.com/me/albums?limit=25&before=NDMyNzQyODI3OTQw"
"next": "https://graph.facebook.com/me/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
}
}

特別的是像 Twitter 這種變動性高的資料,就不是用一般的第幾頁取幾筆的方式,而是用指定 id 向前或後取幾筆的方式,動態找出最新的資料內容。

1
2
user_timeline.json?count=5&max_id=95
user_timeline.json?count=5&since_id=100

自動載入相關(參照)項目的表示法

相較於用戶端重複或多次的去取得相關資料,有提供這功能支援的 API 可提昇使用效率。

基本實作上可利用 embed (或 expand) URL 參數來表示。
embed 用 comma “,” 做欄位區隔,並用 dot “.” 做子欄位參考,如:

GET /products/12?embed=member.name,vendor

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"num" : 12,
"pid_name" : “HP NotePad V101”,
// 以下為 embed 回來的資料
"description" : "Awesome pad around the world!",
"member" : {
"name" : "Bob"
},
vendor: {
"num" : 42,
"name" : "骨頭鳥",
}
}

可載入的資料規則和範圍,要在考慮實作的狀況下預先定義並說明清楚,避免導致過於複雜的狀況發生,如:
N+1 select issue

盡量支援 HATEOAS

何謂 HATEOAS:

HATEOAS (Hypermedia as the Engine of Application State) 是 REST 的約束之一。

它的原則,就是用戶端與伺服器的交互完全由超媒體動態提供,用戶端無需事先瞭解如何與資料或者伺服器交互。簡單講,就是在 response 的內容中包含了連結資訊,用戶端可以根據連結來發現可以執行的動作,不必事先知道後續資源或功能的連結。REST的設計者 Roy T. Fielding 在部落格文章 REST APIs must be hypertext-driven 強調,不是 HATEOAS 的系統不能稱為 RESTful。

HATEOAS 的優點是讓 API可用性變的更高,實現用戶端與服務端的部分解耦。對於不使用 HATEOAS 的 REST 服務,用戶端和伺服器的實現之間是緊密耦合的。用戶端需要根據伺服器提供的相關文檔來瞭解所暴露的資源和對應的操作,當伺服器發生了變化時,如修改了資源的 URI,用戶端也需要進行相應的修改。而使用 HATEOAS 的 REST 服務,用戶端可以通過伺服器提供的資源來自主的發現可以執行的操作,當伺服器發生了變化時,用戶端並不需要做出修改,因為資源的 URI 和其他資訊都是動態發現的。

其實這個概念就和伺服端網頁的產生和操作流程一致,進到首頁後,後續功能和內容連結,都由這個頁面的內容控制。

一種基本的樣式如下: 其中links 中包含連結至其他資源的資訊

1
2
3
4
5
6
7
8
9
{
"payload": json_of_myResult,
"links": [{
"rel": "search",
"href": "http://url.com/api/people/search",
"method": "POST"
},
...]
}

HATEOAS 應用原則

依 API 的應用對象及提供的功能,應最大程度提供 HATEOAS支援。若為沒有介面互動的獨立功能,則可不提供 HATEOAS 的相關資訊。

HATEOAS 是一種思想或者是一種要求,沒有正式規範要如何實作,在 JSON 格式下,有 4 個主要規範:

實務上,可依套件支援程度來選擇提供的格式規範,或參考上面基本樣式實作。

HTTP method 的複寫 (overriding)

有些客戶端只能發出 GET 及 POST,所以 API 可利用自定 Header 在 POST 下提供替代方式。如:
X-HTTP-Method-Override : PUT, PATCH or DELETE

只能在 POST 下,GET 不能做任何異動操作。

額度限制 (Rate limiting)

對於有額度 (數量或時間) 限制的服務,於 API 提供額度資訊,可避免客戶服務突然就被中斷或出錯。

當達到限額,根據 RFC 6585 應回傳 HTTP status code - 429 Too Many Requests 去通知客戶端。

若要預先提供額度資料給客戶端,現在沒有標準規範,以下為常用 Header:

  • X-Rate-Limit-Limit – 這個階段(單位時間內)可用的限量
  • X-Rate-Limit-Remaining – 這個階段剩餘可用量
  • X-Rate-Limit-Reset – 數量重設/到期 時間 (最好用 剩餘秒數 來表示少用 TimeStamp,因為其中包含不必要的時區及日期資訊)

可參考 TwitterGitHub 的文件。

認證 Authentication

當認證失敗,必須回傳 401 Unauthorized statue code 給客戶端。

OAuth2 提供了 token 方式的認證規範,配合各種現成實作,可減少自行處理 token的問題,可優先採用。

認證加授權方案則有 OpenID Connect + OAuth2 的方案。

也可用 JWT 自行定義並實作符合自己所需的規範及流程。

快取 Cache

HTTP 已提供內建的快取機制,只要加上必要的 Response Header 並對 Request Header 做適當的驗證處理即可。

有 2 種方式:ETag and Last-Modified

Last-Modified

流覽器第一次請求 URL 時,回覆 Last-Modified 的 Header 標記此檔最後被修改的時間,格式類似這樣:Tue, 24 Apr 2012 13:53:56 GMT

用戶端第二次請求此 URL 時,流覽器會向伺服器傳送 If-Modified-Since 報頭,詢問是否有被修改過:If-Modified-Since: Tue, 24 Apr 2012 13:53:56 GMT
(日期格式基於 RFC 1123 規格)

如果資源沒有變化,則自動返回 HTTP 304 Not Modified 狀態碼 內容為空,這樣就節省了傳輸資料量。當伺服器端代碼發生改變或者重啟伺服器時,則重新發出資源。

ETag

定義說為 “被請求變數的實體值”。 另一種說法是,ETag 是一個可以與 Web 資源關聯的記號(token)。伺服器負責產生這記號,並在 HTTP 回應 Header 中將其傳送到用戶端,以下是伺服器端返回的格式:ETag: “686897696a7c876b7e”

用戶端的查詢更新格式是這樣的:If-None-Match: “686897696a7c876b7e”

如果 ETag 沒改變,只返回 HTTP 304 Not Modified 不返回資料,這和Last-Modified 一樣。

錯誤訊息

就像一般的網站,API 也應該提供有用清楚的錯誤資訊給客戶端。

首先是狀態碼:

  • 400 系列表示客戶端的問題
  • 500 系列表示伺服器端問題

至少要針對 400 系列的錯誤碼提供標準化的錯誤內容,能擴展到 500 是更好。

額外回覆 JSON 格式的錯誤訊息,可以提供客戶端更詳細的內容和原因。參考格式如下:(Facebook, Twitter … 提供的欄位內容不盡相同)

1
2
3
4
5
{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here"
}

如果有資料欄位驗證錯誤,可提供更詳細的欄位資訊在 errors 的節點,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code" : 1024,
"message" : "Validation Failed",
"errors" : [
{
"code" : 5432,
"field" : "first_name",
"message" : "First name cannot have fancy characters"
},
{
"code" : 5622,
"field" : "password",
"message" : "Password cannot be blank"
}
]
}

HTTP 狀態碼

HTTP 定義了一組的有意義的 HTTP Status Codes,能夠幫助 API 的客戶端清楚知道執行結果並做良好的處理。以下為對用戶端重要的狀態資訊,在開發應該都會用到。

200 OK - 請求已成功,在GET請求中,回應將包含與請求的資源相對應的實體。在POST請求中,回應將包含描述或操作結果的實體。

201 Created - 請求已經被實現,而且有一個新的資源已經依據請求的需要而建立,且其URI已經隨Location標頭資訊返回。

204 No Content - 伺服器成功處理了請求,沒有返回任何內容。(DELETE 可用)

304 Not Modified - 表示資源未被修改,因為請求頭指定的版本If-Modified-Since 或 If-None-Match。在這種情況下,用戶端仍然具有以前下載的副本,因此不需要重新傳輸資源。

400 Bad Request - 由於明顯的用戶端錯誤(例如,格式錯誤,資料量太大,無效的請求訊息或欺騙性路由請求),伺服器不能或不會處理該請求。

401 Unauthorized - 未認證,即用戶沒有必要的憑證,需要用戶做驗證。用戶端可以再提交一個包含適當程序的認證請求(如:登入)。

403 Forbidden - 伺服器已經理解請求(request was valid),但是拒絕執行它,可能是用戶端沒有該資源的存取權。

404 Not Found - 請求失敗,請求的資源未被在伺服器上發現。404 這個狀態碼被廣泛應用於當伺服器不想揭示到底為何請求被拒絕或者沒有其他適合的回應可用的情況下。

405 Method Not Allowed – 請求方法(HTTP method)沒權限使用或不支援。回應必須返回一個 Allow 標頭資訊用以表示出當前資源能夠接受的請求方法的列表。

410 Gone - 表示所請求的資源不再可用。當資源被有意地刪除並且資源應被清除時,應該使用這個。在收到 410 狀態碼後,用戶應停止再次請求資源。但大多數伺服端不會使用此狀態碼,而是直接使用 404 狀態碼。舊版本 API 可利用這個狀態碼做回應。

415 Unsupported Media Type - 請求中提交的 media type 並不是伺服器中所支援的格式,因此請求被拒絕。

422 Unprocessable Entity - 請求格式正確,但是由於含有語意錯誤,無法回應。可用在資料驗證錯誤時。

429 Too Many Requests - 用戶在給定的時間內傳送了太多的請求,用在限量或限速的場合。

結語

API是開發人員的使用者介面。要確保它不僅僅是提供功能,而且要好用。

系列文章