iT邦幫忙

2024 iThome 鐵人賽

DAY 8
1

在前面的文章(第三章)中,我們已經詳細介紹了 HTTP 協議,包括它的結構、請求方法、狀態碼等內容。如果你對 HTTP 協議還不夠熟悉,可以回顧之前的篇章。今天,我們將進一步深入探討 REST API 設計,重點關注如何運用 HTTP 協議來構建高效且擴展性強的 API,並介紹一些進階設計原則與實踐。


什麼是 REST API?

REST API(Representational State Transfer Application Programming Interface)是一種基於 HTTP 協議的 API 設計風格。它通過資源導向的設計,使應用之間的數據交換和操作更加簡單和直觀。REST API 的核心是將「資源」作為中心,並充分利用 HTTP 協議的特性(如方法、狀態碼和 Headers)。

簡單來說,REST API 就像是一個針對資源的服務,每個資源都有自己的 URL,而對這些資源的操作則是通過不同的 HTTP 方法來完成的。


REST API 設計的核心原則

設計一個優秀的 REST API 需要遵循一些核心原則,這不僅能保證功能的實現,也有助於保持 API 的一致性、可擴展性和可維護性。


1. 資源導向設計

REST API 的設計應以「資源」為中心,而非行為或操作。每個資源都應該有一個唯一的 URI(統一資源標識符),資源通常是應用中的某個實體(如「用戶」或「訂單」)。設計時,應通過 URI 來訪問具體資源。

範例:

  • /users:表示所有用戶的資源集合。
  • /users/123:表示 ID 為 123 的具體用戶資源。

這樣的設計使得 API 使用者可以輕易理解 API 的結構,並直觀地知道如何訪問或操作資源。


2. 正確使用 HTTP 方法

HTTP 方法定義了對資源應該進行的操作。在 REST API 中,正確使用這些方法至關重要,因為它們賦予 API 自然且一致的行為。

  • GET:檢索資源,用於從伺服器獲取資料。這是一個安全且無副作用的操作。
  • POST:創建資源,用於向伺服器提交數據並創建新資源。這是有副作用的操作。
  • PUT:更新資源,替換現有資源的整體內容,是冪等操作(多次執行結果一致)。
  • PATCH:部分更新資源,靈活地修改現有資源的一部分。
  • DELETE:刪除資源,移除指定的資源,也是冪等操作。

正確的操作方法可以提升 API 的一致性與可讀性,讓開發者清楚知道每個請求應該如何工作。


3. 無狀態性

REST API 是無狀態的,每一次的請求都是獨立的。伺服器不會保留客戶端的上下文信息,因此每個請求必須包含完成操作所需的所有信息。這種設計讓 API 更容易擴展,因為請求可以分散到不同的伺服器進行處理。

範例:

GET /users/123
Host: api.example.com
Authorization: Bearer <token>

這樣的請求包含了所有必需的信息,伺服器無需依賴之前的請求上下文。


4. 正確使用 HTTP 狀態碼

REST API 通過 HTTP 狀態碼反饋請求結果。正確地使用狀態碼能夠幫助 API 使用者快速了解請求的結果或錯誤原因。

常見狀態碼:

  • 200 OK:請求成功,並返回資源。
  • 201 Created:創建成功(適用於 POST 請求)。
  • 204 No Content:請求成功,但無需返回資源(適用於 DELETE 請求)。
  • 400 Bad Request:無效請求。
  • 401 Unauthorized:未經授權,請求需要身份驗證。
  • 404 Not Found:資源不存在。
  • 500 Internal Server Error:伺服器內部錯誤。

5. 支持過濾、分頁和排序

當 API 返回大量數據時,應該支持過濾、分頁和排序,這不僅提高了效率,也使得客戶端能夠靈活地篩選和處理數據。

範例:

GET /users?age=25&limit=10&page=2&sort=created_at

這樣的請求會過濾出年齡為 25 的用戶,並按創建時間排序,返回第二頁的 10 個結果。


6. 避免過度設計

REST API 設計應保持簡潔和易於使用。過度設計會讓 API 變得難以理解和維護。應避免在 URI 中混入動詞或具體操作,如 /getAllUsers/deleteUserById。這些操作應通過 HTTP 方法來實現,而非通過 URI。


進階設計:版本控制與擴展性

隨著 API 的演變,應用需求的變化可能會影響 API 的設計。因此,版本控制和擴展性在設計過程中是不可忽視的要素。


1. 版本控制

API 需要隨著時間推進進行升級或變更,因此保持對現有使用者的兼容性至關重要。常見的版本控制方法包括:

  • URI 版本控制:如 /v1/users
  • Header 版本控制:通過 HTTP Header 指定版本號,如 Accept: application/vnd.example.v1+json
  • 參數版本控制:使用查詢參數來指定版本,如 /users?version=1

在實際應用中,URI 版本控制最為常見和直觀。


2. 向後兼容與擴展性

在設計 API 時,要盡量避免對現有字段的移除或修改。應通過新增字段或路由來進行擴展,以保證對現有使用者的兼容性。

範例:

{
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com",
    "age": 30,
    "new_field": "new_value"  // 新增字段,保持向後兼容
}

這樣設計的 API 可以保持現有功能不變,並允許新需求的擴展。


不好的 REST API 設計範例

以下是一個設計不佳的 REST API 範例,並解釋了其中的問題:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private static List<User> Users = new List<User>
    {
        new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 },
        new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 }
    };

    // GET: api/users
    [HttpGet("action")]//設計錯誤: 違背了資源導向的設計原則
    public ActionResult<IEnumerable<User>> GetAllUsers()
    {
        return Ok(Users);
    }

    // POST: api/users/action/add
    [HttpPost("action/add")]//設計錯誤: 違背了資源導向的設計原則
    public ActionResult<User> CreateNewUser([FromBody] User newUser)
    {
        newUser.Id = Users.Max(u => u.Id) + 1;
        Users.Add(newUser);
        return Ok(newUser); // 設計錯誤:應該返回 201 Created
    }

    // PUT: api/users/action/update/{id}
    [HttpPut("action/update/{id}")]//設計錯誤: 違背了資源導向的設計原則
    public ActionResult UpdateUserDetails(int id, [FromBody] User updatedUser)
    {
        var user = Users.FirstOrDefault(u => u.Id == id);
        if (user == null)
        {
            return NotFound();
        }
        user.Name = updatedUser.Name;
        user.Email = updatedUser.Email;
        user.Age = updatedUser.Age;
        return Ok(user); // 設計錯誤:應該返回 204 No Content
    }

    // DELETE: api/users/action/delete/{id}
    [HttpDelete("action/delete/{id}")]//設計錯誤: 違背了資源導向的設計原則
    public ActionResult RemoveUser(int id)
    {
        var user = Users.FirstOrDefault(u => u.Id == id);
        if (user == null)
        {
            return NotFound();
        }
        Users.Remove(user);
        return Ok(); // 設計錯誤:應該返回 204 No Content
    }

    // 自定義操作方法,存在設計缺陷
	[HttpPost("custom-action")] // 設計錯誤:不明確的操作,無法清楚理解該操作的目的
	public ActionResult CustomOperation([FromBody] CustomRequest request)
	{
	    // 根據 OperationType 字段來決定具體的操作
	    if (request.OperationType == "updateEmail")
	    {
	        var user = Users.FirstOrDefault(u => u.Id == request.UserId);
	        if (user == null)
	        {
	            return NotFound("User not found");
	        }
	        user.Email = request.Data; // 更新用戶的電子郵件
	    }
	    else if (request.OperationType == "deleteUser")
	    {
	        var user = Users.FirstOrDefault(u => u.Id == request.UserId);
	        if (user == null)
	        {
	            return NotFound("User not found");
	        }
	        Users.Remove(user); // 刪除用戶
	    }
	    else if (request.OperationType == "addNote")
	    {
	        var user = Users.FirstOrDefault(u => u.Id == request.UserId);
	        if (user == null)
	        {
	            return NotFound("User not found");
	        }
	        // 為用戶添加備註(在現有的資料上增加新數據)
	        // 假設用戶模型中存在 Notes 屬性
	        user.Notes.Add(request.Data);
	    }
	    else
	    {
	        return BadRequest("Unknown operation");
	    }
	
	    // 無論進行什麼操作,最終都返回相同的回應
	    return Ok("Custom operation performed");
	}
}

public class CustomRequest
{
    public string OperationType { get; set; }
    public int UserId { get; set; }
    public string Data { get; set; }
}

為什麼這個設計不佳?

  1. 混亂的 URI 設計
    • URI 包含 action 和其他具體操作動詞,如 action/add,這違背了資源導向的設計原則。應將 URI 專注於資源,而非操作行為。
    • 改進建議:應使用 /users 來表示所有用戶,使用 /users/{id} 來表示具體用戶,而不是 action/add 等不清晰的操作。
  2. 不正確的 HTTP 方法使用
    • CreateNewUserUpdateUserDetails 中,錯誤地使用了 200 OK 作為回應。正確的設計應該是使用 201 Created204 No Content
    • 改進建議:對於創建操作,應使用 201 Created,並返回新資源的 URI;對於更新操作,應使用 204 No Content
  3. 錯誤的狀態碼返回
    • 在刪除和更新操作中返回了 200 OK,而不是更合適的 204 No Content,這讓使用者無法準確理解操作結果。
  4. 不明確的操作
    • CustomOperation 使用 OperationType 來決定執行的邏輯,這違反了單一職責原則,增加了 API 的複雜度,讓使用者難以理解具體的操作內容。
    • 改進建議:將不同的操作分開,給每個操作設計專門的 API 端點,而非依賴於操作參數。

針對第4點「不明確的操作」特別說明~為什麼這樣的設計是錯誤的?

  1. 操作多樣且含混不清
    • 這個 API 端點 custom-action 允許一個請求執行多種不同的操作,例如更新用戶的電子郵件、刪除用戶、或添加備註。
    • 當 API 使用者看到這個端點時,他們無法一眼確定它會執行什麼操作,因為具體的行為是由 OperationType 字段決定的。這與 REST API 的設計原則背道而馳,因為 REST API 強調「資源導向」,每個操作應該有一個明確且一致的 URI 和 HTTP 方法。
  2. 多個行為綁定在一個端點上
    • 在同一個端點中處理多種不相關的行為,違反了單一職責原則。API 使用者需要理解並處理多種操作類型,這增加了使用 API 的複雜度。
    • 例如,updateEmail 應該有一個獨立的端點,如 PUT /users/{id}/email,而 deleteUser 應該對應 DELETE /users/{id},這樣使用者可以直觀地知道每個端點具體執行什麼操作。
  3. 返回一致但無意義的結果
    • 無論請求進行了什麼操作,最後總是返回相同的回應 "Custom operation performed"。這並沒有提供足夠的上下文來告訴客戶端實際完成了什麼操作。如果發生錯誤或某些操作部分完成,用戶將無法得知具體情況。
  4. 不合適的狀態碼使用
    • 這個 API 端點對於每一個操作,無論是更新、刪除還是添加,最終都會返回 200 OK。然而,正確的做法是針對不同操作使用相應的狀態碼。例如:
      • 對於更新操作,應返回 204 No Content
      • 對於創建新資源或數據,應返回 201 Created
      • 對於刪除操作,應返回 204 No Content

改進後的 REST API 設計範例

[ApiController] 
[Route("api/[controller]")] // 定義這個控制器的路由,"api/[controller]" 會自動將控制器名稱替換為 "users"
public class UsersController : ControllerBase
{
    // 模擬用戶資料的靜態列表
    private static List<User> Users = new List<User>
    {
        new User { Id = 1, Name = "John Doe", Email = "john@example.com", Age = 30 }, // 預設用戶 1
        new User { Id = 2, Name = "Jane Smith", Email = "jane@example.com", Age = 25 } // 預設用戶 2
    };

    // GET: api/users
    [HttpGet] // 定義 GET 請求,用於獲取所有用戶資料
    public ActionResult<IEnumerable<User>> GetUsers()
    {
        // 返回 200 OK 狀態碼,並傳回用戶列表
        return Ok(Users);
    }

    // POST: api/users
    [HttpPost] // 定義 POST 請求,用於創建新的用戶
    public ActionResult<User> CreateUser([FromBody] User newUser)
    {
        // 分配新用戶的 ID,使用現有用戶的最大 ID 加 1
        newUser.Id = Users.Max(u => u.Id) + 1;
        // 將新用戶加入用戶列表
        Users.Add(newUser);
        // 返回 201 Created 狀態碼,並附上新創建用戶
        return CreatedAtAction(nameof(GetUsers), new { id = newUser.Id }, newUser);
    }

    // PUT: api/users/{id}
    [HttpPut("{id}")] // 定義 PUT 請求,用於更新指定 ID 的用戶資料
    public ActionResult UpdateUser(int id, [FromBody] User updatedUser)
    {
        // 根據 ID 查找現有用戶
        var user = Users.FirstOrDefault(u => u.Id == id);
        // 如果用戶不存在,返回 404 Not Found
        if (user == null)
        {
            return NotFound();
        }
        // 更新用戶的資料
        user.Name = updatedUser.Name;
        user.Email = updatedUser.Email;
        user.Age = updatedUser.Age;
        // 返回 204 No Content,表示更新成功但不需要回傳內容
        return NoContent();
    }

    // DELETE: api/users/{id}
    [HttpDelete("{id}")] // 定義 DELETE 請求,用於刪除指定 ID 的用戶
    public ActionResult DeleteUser(int id)
    {
        // 根據 ID 查找現有用戶
        var user = Users.FirstOrDefault(u => u.Id == id);
        // 如果用戶不存在,返回 404 Not Found
        if (user == null)
        {
            return NotFound();
        }
        // 從列表中移除用戶
        Users.Remove(user);
        // 返回 204 No Content,表示刪除成功但不需要回傳內容
        return NoContent();
    }
}

每日小結

設計一個優秀的 REST API 不僅需要實現功能,還必須著眼於 API 的可用性、可維護性和擴展性。堅持資源導向設計、正確使用 HTTP 方法和狀態碼、保持無狀態性、並考慮版本控制和擴展性,這些設計原則能讓 API 更加一致、易於使用且具備彈性。通過避免常見的設計錯誤,我們可以提升 API 的品質,讓使用者在開發和使用過程中獲得更好的體驗。


上一篇
Day 7 三大API架構設計
下一篇
Day 9 非同步概念
系列文
使用 C# 從零開始玩轉 Web API,從基礎到微服務與雲端部署的全面探索22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言