RESTful Web API 設計指南
REST 架構如何使用 HTTP 方法操作資源和有效運用 HTTP 協定的狀態碼,以及網頁應用程式後端如何規劃優良並易於前端使用 RESTful Web API 的設計原則及最佳實作建議指南。
URI 端點
端點 (endpoint) 指的是用於訪問 API (應用程式介面) 的 URI (統一資源標誌符),其實就是網址。由於 API 會將多種不同的功能進行封裝,因此會有許多個不同的端點。
以公司為例,如果要通過 API 取得員工和部門資源 (resource),就需要設計兩個不同的 URI 端點,而要對資源進行什麼操作就要使用 HTTP 方法,例如 GET 方法可用來取得資源組 (所有資料) 或指定的一筆資源 (一筆資料)。
- 員工:https://api.example.com/v1/employees。
- 部門:https://api.example.com/v1/departments。
設計原則
URI 設計原則如下:
- 簡短便於輸入的 URI:去除無意義且重覆的單詞,例如 https://api.example.com/api/v1/employees,由 domain 就能得知這是一個 API,但在路徑又重覆了一次 api。
- 使用者能讀懂的 URI:使用有意義的英文單字,而不使用縮寫。
- 小寫英文字母的 URI:一律使用小寫英文字母,更不可大小寫字母混用。
- 修改方便的 URI:例如 https://api.example.com/v1/employees/1,即可看出該員工 ID 為 1,而且只要修改 ID 就能獲取其他員工的資源。
- 不暴露 server 架構的 URI:例如 https://api.example.com/v1/employees.php,就知道應該是用 PHP 程式語言編寫的,這讓黑客更容易針對漏洞發起惡意攻擊。
- 規則統一的 URI:例如使用英文字母複數 https://api.example.com/v1/employees 來取得所有員工資源組,但確用單數 https://api.example.com/v1/employee/1 來取得一位員工的資源,類似這種不統一的 URI 容易造成 client 的混亂。
- 使用英文單字名詞的複數。
- 不使用空格及需要編碼的字元。
- 使用連接符 - 連結多個英文單字:但應避免多個英文單字連結,而是使用路徑 / 劃分的方式。
- URI 應加入 API 的版本號:如 https://api.example.com/v1/employees。
HTTP 方法和 API 端點設計
URI 指定要使用的 API 資源,而 HTTP 方法就是要對資源進行什麼操作,RESTful Web API 會使用到的 HTTP 方法對應如下表:
方法 | 一組資源 URI:https://api.example.com/v1/employees | 單個資源 URI:https://api.example.com/v1/employees/1 |
---|---|---|
GET | 取得資源組 (所有資料) | 取得指定的一筆資源 |
POST | 新增一筆資源 | |
PUT | 覆蓋指定的一筆資源 | |
PATCH | 更新指定的一筆資源 (部份更新) | |
DELETE | 刪除指定的一筆資源 |
資料筆數與位置查詢參數
資料量如果很多的話,API 就要設計成能夠指定欲取得的資料筆數 limit
和資料位置 offset
or max_id
,這兩個查詢參數也是 client 實現分頁機制所須要的。
項目 | 查詢參數名稱 | 參數值 | 說明 | 備註 | |
---|---|---|---|---|---|
資料筆數 | limit | 5 | 每次取得的資料量 | ||
資料位置 | 相對位置 | offset | 10 | 識別已取得的資料位置 | 從 0 (0-based) 開始計數 |
絕對位置 | max_id | 1 | 識別已取得的資料位置 | 使用唯一值,如員工 ID |
相對位置
相對位置使用查詢參數 offset
指定從哪筆資料開始,對於 server 實作較簡單,但這種方式存在二個問題:
- 效能不佳:資料庫 (database) 為了要取得資料是從哪個位置開始,每次都要從第 1 筆資料開始計數,因此會隨著儲存的資料量增加,導致查詢速度越來越慢。例如在 MySQL 使用
LIMIT
與OFFSET
來指定要取得的資料筆數與位置時。 - 資料偏差:當一開始取得 5 筆資料後,正準備取得緊接著的 5 筆資料,如果這段期間內有新增資料,將會導致取得重複的資料,刪除資料也存在這個問題。
例如從第 2 筆資料位置開始 (offset
從 0 開始計數),取得 2 筆資料量的查詢參數:
https://api.example.com/v1/employees?limit=2&offset=1
絕對位置
不同於相對位置使用查詢參數 offset
指定從哪筆資料開始,絕對位置則是使用查詢參數 max_id
指定從某 ID 開始的方式,即可解決相對位置效能不佳和資料偏差的問題,可參考 Working with timelines — Twitter Developers。
絕對位置在第一次的請求,僅須使用查詢參數 limit
指定要取得的資料量,接著 client 會記錄當前已取得最後一筆資料的 ID,在之後的每次請求就必須將此 ID 加入查詢參數 max_id
。
例如從員工 ID 為 1 的資料位置開始,取得 2 筆資料量:
http://192.168.0.200:3000/v1/employees?limit=2&max_id=1
過濾查詢參數設計
例如在資源 https://api.example.com/v1/employees 使用 GET 方法取得的資源組如下:
{
"employees": [
{
"id": 1,
"name": "王二",
"address": "台南市xx路"
},
{
"id": 2,
"name": "張三",
"address": "台北市xx路"
},
{
"id": 3,
"name": "李四",
"address": "台南市xx路"
}
]
}
完全匹配
完全匹配的查詢參數名稱與取得資料的欄位名稱對應,查詢參數值則是要過濾的資料,如要過濾多筆資料可使用逗號 , 分隔指定多個。
取得員工姓名王二的資料
查詢參數:
https://api.example.com/v1/employees?name=王二
SQL:
SELECT *
FROM employees
WHERE name = '王二'
回應的資料:
{
"employees": [
{
"id": 1,
"name": "王二",
"address": "台南市xx路"
}
]
}
取得員工姓名王二和張三的資料
查詢參數:
https://api.example.com/v1/employees?name=王二,張三
SQL:
SELECT *
FROM employees
WHERE name = '王二' OR name = '張三'
回應的資料:
{
"employees": [
{
"id": 1,
"name": "王二",
"address": "台南市xx路"
},
{
"id": 2,
"name": "張三",
"address": "台北市xx路"
}
]
}
部分匹配
針對單一欄位的情況下,使用查詢參數名稱 q
(query) 來表示部份匹配,假設這裡指定的單一欄位為 address。
取得住址在台南市的員工
查詢參數:
https://api.example.com/v1/employees?q=台南市
SQL:
SELECT *
FROM employees
WHERE address LIKE '%台南市%'
回應的資料:
{
"employees": [
{
"id": 1,
"name": "王二",
"address": "台南市xx路"
},
{
"id": 3,
"name": "李四",
"address": "台南市xx路"
}
]
}
排序查詢參數設計
使用查詢參數 sortby
來設定使用哪個欄位排序,在搭配查詢參數 order
的值來設定排序的方式,上升 asc
或下降 desc
。
使用 id 下降排序
查詢參數:
https://api.example.com/v1/employees?sortby=id&order=desc
SQL:
SELECT *
FROM employees
ORDER BY id DESC
回應的資料:
{
"employees": [
{
"id": 3,
…
},
{
"id": 2,
…
},
{
"id": 1,
…
}
]
}
回應資料的設計
client 可選擇回應內容
一般都會回應所有欄位的資料,但有些時候 client 可能僅需要某些欄位,這時就要設計成能透過查詢參數 fields
來指定僅需要哪些欄位而已。
僅取得員工 ID 欄位的資料
查詢參數:
https://api.example.com/v1/employees?fields=id
SQL:
SELECT id
FROM employees
回應的資料:
{
"employees": [
{
"id": 1
},
{
"id": 2
},
…
]
}
僅取得員工 ID 和姓名欄位的資料
使用逗號 , 分隔指定多個參數值。
查詢參數:
https://api.example.com/v1/employees?fields=id,name
SQL:
SELECT id, name
FROM employees
回應的資料:
{
"employees": [
{
"id": 1,
"name": "王二"
},
{
"id": 2,
"name": "張三"
},
…
]
}
封裝資料格式 object 和 array
JSON 雖然可以儲存 JavaScript 的 object (物件) 或 array (陣列) 格式的資料,但建議一律統一使用 object 格式,因為它有幾項優點:
- 回應的資料較容易理解:由於所有 array 資料包含在一個 object 的屬性名稱內,因此從屬性名稱就能識別這是什麼內容的資料。
- 避免 JSON 注入 (injection) 的安全性風險:能避免使用
<script>
元素加載 JSON,並在瀏覽器裡加載入其它 API 服務所提供的 JSON,從而非法獲得之中的訊息。
object 格式:
{
"employees": [
{
"id": 1,
"name": "王二",
…
},
{
"id": 2,
"name": "張三",
…
},
…
]
}
array 格式:
[
{
"id": 1,
"name": "王二",
…
},
{
"id": 2,
"name": "張三",
…
},
…
]
總資料量與後續是否有資料
client 要實作分頁功能或要知道後續是否還有資料時,須透過回應的兩個名稱來識別:
total
:總資料量,使用數值表示。hasNext
:後續是否有資料,true 有資料 / false 無資料。
limit
查詢參數來取得資料量時,也會一併回應 total 總資料量。{
"total": 27,
"hasNext": false
}
資料的格式
JSON 名稱命名
單個英文單字的名稱都使用小寫命名,但如果是由多個英文單字組合而成的名稱呢!
因為 JSON 是由 JavaScript 發展而來的,所以也使用與 JavaScript 相同的駝峰式大小寫的「小駝峰式命名法」,也就是從第二個英文單字之後的第一個字母均大寫,例如 employeeId、firstName、lastName、loginDataTime。
由於 RESTful Web API 與資料庫關聯,而資料表欄位對多個英文單字組合的命名方式卻是使用底線 _,例如 employee_id、first_name、last_name、login_data_time,因此有時為了不必在 client 與 server 轉換,就不用太執著名稱的命名,統一使用底線 _ 的方式也可以。
錯誤訊息的表示
雖然可透過 HTTP 狀態碼 4xx、5xx 得知通用的錯誤描述,但仍無法準確表達出 RESTful Web API 實際所發生的錯誤原因,所以要在發生錯誤時一併回應詳細的錯誤訊息和錯誤代碼給 client。自訂的錯誤代碼建議使用 4 位數來和 HTTP 狀態碼的 3 位數區隔以避免混沌,但可採用類似 HTTP 狀態碼的分類方式,例如第一位數 1xxx 為通用錯誤,2xxx 為查詢參數錯誤。
回應的錯誤訊息:
{
"errors": {
"message": "詳細錯誤資訊 …",
"code": 2015
}
}
有效運用 HTTP 協定
RESTful Web API 通過 HTTP 協定來完成通訊,因此要充分理解 HTTP 協定,才能在使用上更加上手。
HTTP 會話由一對請求和回應各自包含下述依序的四個內容組成:
- 行:
- 請求:使用什麼方法,例如 GET /v1/employees HTTP/1.1。
- 回應:請求的結果,例如 HTTP/1.1 200 OK。
- 首部 (詳細資訊可參考 HTTP Headers - HTTP | MDN):
- 請求:包含要獲取的資源或 client 本身的訊息,例如 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) ...。
- 回應:包含補充訊息,例如 Server: Apache/2.4.12 (Win32) OpenSSL/1.0.1l PHP/5.6.8。
- 空行:必須只有 <CR> <LF> 而無其他空格,用來解析首部已經結束了。
- 訊息體:
- 請求:client 發送給 server 的資料,例如
fields=id
。 - 回應:server 回應給 client 的資料,例如
{"employees":[{"id":1,"name":"王二"}]}
。
- 請求:client 發送給 server 的資料,例如
正確使用 HTTP 狀態碼
狀態碼是在 HTTP 回應訊息首部開頭必填的 3 位數字,狀態碼的用途為 client 請求發送至 server 進行處理後的狀態,也就是用來讓 client 判別請求是否被 server 正確地處理,詳細的 HTTP 說明可參考 AJAX JavaScript 與 jQuery 教學範例 for PHP。
透過 HTTP 狀態碼首位數字即可了解大概含義:
狀態碼 | 含義 | 說明 |
---|---|---|
1 開頭 | 訊息 | 請求被接受,需要繼續處理。用途為臨時回應 |
2 開頭 | 成功 | 請求成功被伺服器接收、理解、並接受 |
3 開頭 | 重定導向 | client 採取進一步的操作才能完成請求 |
4 開頭 | client 引起的錯誤 | client 錯誤導致 server 不能或不會處理請求 |
5 開頭 | server 引起的錯誤 | server 在處理請求的過程中發生錯誤或異常 |
狀態碼 | 名稱 | 說明 | API 使用場景 |
---|---|---|---|
200 | OK | 請求成功 | GET、PUT、PATCH 方法,取得資料 |
201 | Created | 新的資源已建立 | POST 方法,新增資料 |
204 | No Content | 沒有返回任何內容 | DELETE 方法,刪除資料 |
400 | Bad Request | 請求不正確 | 其它 4 開頭狀態碼沒有合適的,如參數錯誤 |
401 | Unauthorized | 用戶需要認證 | |
403 | Forbidden | 禁止訪問,與 401 回應不同的是,帳戶己認證,但沒有權限 | |
404 | Not Found | 沒有找到指定的資源 | GET、PUT、PATCH、DELETE 方法,該資料不存在 |
500 | Internal Server Error | 伺服器發生錯誤 |
參考
本著作係採用創用 CC 姓名標示-相同方式分享 3.0 台灣 授權條款授權.