RESTful Web API 設計指南 - MIS 腳印
MIS 腳印 logo

MIS 腳印

記錄 IT 學習的軌跡

RESTful Web API 設計指南

REST 架構如何使用 HTTP 方法操作資源和有效運用 HTTP 協定的狀態碼,以及網頁應用程式後端如何規劃優良並易於前端使用 RESTful Web API 的設計原則及最佳實作建議指南。

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 設計原則如下:

  1. 簡短便於輸入的 URI:去除無意義且重覆的單詞,例如 https://api.example.com/api/v1/employees,由 domain 就能得知這是一個 API,但在路徑又重覆了一次 api。
  2. 使用者能讀懂的 URI:使用有意義的英文單字,而不使用縮寫。
  3. 小寫英文字母的 URI:一律使用小寫英文字母,更不可大小寫字母混用。
  4. 修改方便的 URI:例如 https://api.example.com/v1/employees/1,即可看出該員工 ID 為 1,而且只要修改 ID 就能獲取其他員工的資源。
  5. 不暴露 server 架構的 URI:例如 https://api.example.com/v1/employees.php,就知道應該是用 PHP 程式語言編寫的,這讓黑客更容易針對漏洞發起惡意攻擊。
  6. 規則統一的 URI:例如使用英文字母複數 https://api.example.com/v1/employees 來取得所有員工資源組,但確用單數 https://api.example.com/v1/employee/1 來取得一位員工的資源,類似這種不統一的 URI 容易造成 client 的混亂。
  7. 使用英文單字名詞的複數。
  8. 不使用空格及需要編碼的字元。
  9. 使用連接符 - 連結多個英文單字:但應避免多個英文單字連結,而是使用路徑 / 劃分的方式。
  10. URI 應加入 API 的版本號:如 https://api.example.com/v1/employees。

HTTP 方法和 API 端點設計

URI 指定要使用的 API 資源,而 HTTP 方法就是要對資源進行什麼操作,RESTful Web 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 實現分頁機制所須要的。

RESTful Web API 資料筆數與位置查詢參數
項目 查詢參數名稱 參數值 說明 備註
資料筆數 limit 5 每次取得的資料量
資料位置 相對位置 offset 10 識別已取得的資料位置 從 0 (0-based) 開始計數
絕對位置 max_id 1 識別已取得的資料位置 使用唯一值,如員工 ID

相對位置

相對位置使用查詢參數 offset 指定從哪筆資料開始,對於 server 實作較簡單,但這種方式存在二個問題:

  • 效能不佳:資料庫 (database) 為了要取得資料是從哪個位置開始,每次都要從第 1 筆資料開始計數,因此會隨著儲存的資料量增加,導致查詢速度越來越慢。例如在 MySQL 使用 LIMITOFFSET 來指定要取得的資料筆數與位置時。
  • 資料偏差:當一開始取得 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 無資料。
{
    "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 會話由一對請求和回應各自包含下述依序的四個內容組成:

  1. 行:
    • 請求:使用什麼方法,例如 GET /v1/employees HTTP/1.1。
    • 回應:請求的結果,例如 HTTP/1.1 200 OK。
  2. 首部 (詳細資訊可參考 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。
  3. 空行:必須只有 <CR> <LF> 而無其他空格,用來解析首部已經結束了。
  4. 訊息體:
    • 請求:client 發送給 server 的資料,例如 fields=id
    • 回應:server 回應給 client 的資料,例如 {"employees":[{"id":1,"name":"王二"}]}

正確使用 HTTP 狀態碼

狀態碼是在 HTTP 回應訊息首部開頭必填的 3 位數字,狀態碼的用途為 client 請求發送至 server 進行處理後的狀態,也就是用來讓 client 判別請求是否被 server 正確地處理,詳細的 HTTP 說明可參考 AJAX JavaScript 與 jQuery 教學範例 for PHP

透過 HTTP 狀態碼首位數字即可了解大概含義:

HTTP 狀態碼首位數字含義
狀態碼 含義 說明
1 開頭 訊息 請求被接受,需要繼續處理。用途為臨時回應
2 開頭 成功 請求成功被伺服器接收、理解、並接受
3 開頭 重定導向 client 採取進一步的操作才能完成請求
4 開頭 client 引起的錯誤 client 錯誤導致 server 不能或不會處理請求
5 開頭 server 引起的錯誤 server 在處理請求的過程中發生錯誤或異常
RESTful Web API 常用 HTTP 狀態碼
狀態碼 名稱 說明 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 伺服器發生錯誤  

參考

發表迴響