☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
ASP.NET Core 中級

🔗 REST API 設計最佳實踐

📌 什麼是 REST?

REST(Representational State Transfer)是一種設計 API 的風格,不是一種技術。它定義了如何透過 HTTP 協議來操作資源。

想像你經營一家圖書館

  • 資源(Resource) = 書本、會員、借閱記錄
  • URL = 書本的分類編號(告訴你去哪個書架找)
  • HTTP 方法 = 你要做什麼操作(借書、還書、查書)
  • 回應 = 圖書館員給你的答覆和書本

📜 REST 的六大原則

1. 客戶端-伺服器(Client-Server)
   → 像餐廳:客人點餐,廚房做菜,分工明確

2. 無狀態(Stateless)
   → 像售票機:每次購票都要重新選擇,不會記住你上次買了什麼

3. 可快取(Cacheable)
   → 像報紙:今天的頭條可以看好幾次,不用每次都重新印

4. 統一介面(Uniform Interface)
   → 像 USB 接口:不管什麼裝置,插口都一樣

5. 分層系統(Layered System)
   → 像大公司的組織架構:經理不需要知道工廠的細節

6. 按需代碼(Code on Demand,選擇性)
   → 像餐廳提供的食譜:客人可以自己回家做,也可以不用

🛤️ URL 命名規範

// ✅ 好的 URL 設計(名詞、複數、小寫、用破折號分隔)
// 控制器範例
[Route("api/[controller]")] // 基底路由
[ApiController] // 標記為 API 控制器
public class ProductsController : ControllerBase // 商品控制器
{
    // GET /api/products          → 取得所有商品
    [HttpGet] // 對應 GET 方法
    public IActionResult GetAll() => Ok(products); // 回傳所有商品

    // GET /api/products/5        → 取得單一商品
    [HttpGet("{id}")] // 路由參數
    public IActionResult GetById(int id) => Ok(product); // 回傳指定商品

    // POST /api/products         → 建立新商品
    [HttpPost] // 對應 POST 方法
    public IActionResult Create(Product p) => Created($"/api/products/{p.Id}", p);

    // PUT /api/products/5        → 更新整個商品
    [HttpPut("{id}")] // 對應 PUT 方法
    public IActionResult Update(int id, Product p) => NoContent(); // 更新商品

    // DELETE /api/products/5     → 刪除商品
    [HttpDelete("{id}")] // 對應 DELETE 方法
    public IActionResult Delete(int id) => NoContent(); // 刪除商品
}
❌ 不好的 URL 設計:
/api/getProducts         → 動詞不該出現在 URL 中
/api/product             → 應該用複數 products
/api/Products            → 應該用小寫 products
/api/delete-product/5    → 用 HTTP DELETE 方法,不要把動詞放在 URL
/api/product_list        → 用破折號,不要用底線

✅ 好的 URL 設計:
/api/products            → 取得所有商品(GET)
/api/products/5          → 取得 ID=5 的商品(GET)
/api/products            → 建立新商品(POST)
/api/products/5          → 更新 ID=5 的商品(PUT)
/api/products/5          → 刪除 ID=5 的商品(DELETE)
/api/products/5/reviews  → 取得商品 5 的所有評論(子資源)

📄 分頁、篩選與排序

// API 端點:支援分頁、篩選和排序
[HttpGet] // GET /api/products?page=1&pageSize=10&sort=price&order=desc&category=electronics
public IActionResult GetProducts(
    [FromQuery] int page = 1,         // 第幾頁(預設第 1 頁)
    [FromQuery] int pageSize = 10,    // 每頁幾筆(預設 10 筆)
    [FromQuery] string? sort = null,  // 排序欄位
    [FromQuery] string? order = "asc", // 排序方向:asc 或 desc
    [FromQuery] string? category = null) // 篩選條件
{
    var query = _db.Products.AsQueryable(); // 建立可查詢的集合

    // 篩選(像圖書館用分類找書)
    if (!string.IsNullOrEmpty(category)) // 如果有指定分類
    {
        query = query.Where(p => p.Category == category); // 篩選符合的商品
    }

    // 排序(像圖書館按照書名或出版日期排列)
    query = sort?.ToLower() switch
    {
        "price" => order == "desc"
            ? query.OrderByDescending(p => p.Price)  // 價格由高到低
            : query.OrderBy(p => p.Price),           // 價格由低到高
        "name" => order == "desc"
            ? query.OrderByDescending(p => p.Name)   // 名稱 Z 到 A
            : query.OrderBy(p => p.Name),            // 名稱 A 到 Z
        _ => query.OrderBy(p => p.Id)                // 預設用 ID 排序
    };

    // 計算總數(在分頁之前)
    var totalCount = query.Count(); // 總共有幾筆資料

    // 分頁(像一本書分成好幾頁,一次只看一頁)
    var items = query
        .Skip((page - 1) * pageSize) // 跳過前面的資料
        .Take(pageSize)              // 只取這一頁的數量
        .ToList();                   // 執行查詢

    // 回傳分頁資訊
    var result = new
    {
        Data = items,              // 這一頁的資料
        Page = page,               // 目前第幾頁
        PageSize = pageSize,       // 每頁幾筆
        TotalCount = totalCount,   // 總筆數
        TotalPages = (int)Math.Ceiling((double)totalCount / pageSize) // 總頁數
    };

    return Ok(result); // 回傳 200 OK
}

🔗 HATEOAS 概念

// HATEOAS(Hypermedia As The Engine Of Application State)
// 簡單說:API 回應中包含「接下來可以做什麼」的連結
// 像餐廳菜單上不只有菜名,還有「加點」「套餐升級」的選項

[HttpGet("{id}")] // 取得單一商品
public IActionResult GetById(int id)
{
    var product = _db.Products.Find(id); // 查找商品
    if (product == null) // 如果找不到
        return NotFound(); // 回傳 404

    // 回傳資料時附帶相關操作的連結
    var result = new
    {
        Data = product, // 商品資料
        Links = new[]   // 可以執行的操作連結
        {
            new { Rel = "self", Href = $"/api/products/{id}", Method = "GET" },
            new { Rel = "update", Href = $"/api/products/{id}", Method = "PUT" },
            new { Rel = "delete", Href = $"/api/products/{id}", Method = "DELETE" },
            new { Rel = "reviews", Href = $"/api/products/{id}/reviews", Method = "GET" }
        }
    };

    return Ok(result); // 回傳 200 OK 和連結
}

🏷️ API 版本控制策略

// 策略 1:URL 路徑版本控制(最常用,像書的版次)
[Route("api/v1/products")] // 第一版
public class ProductsV1Controller : ControllerBase
{
    [HttpGet] // GET /api/v1/products
    public IActionResult GetAll() => Ok("V1 格式的商品清單"); // V1 格式回傳
}

[Route("api/v2/products")] // 第二版
public class ProductsV2Controller : ControllerBase
{
    [HttpGet] // GET /api/v2/products
    public IActionResult GetAll() => Ok("V2 格式的商品清單(更多欄位)"); // V2 格式回傳
}

// 策略 2:HTTP Header 版本控制
// 請求時帶上 Header:api-version: 2
[HttpGet]
public IActionResult GetAll([FromHeader(Name = "api-version")] int version = 1)
{
    if (version == 2) // 如果要求 V2
        return Ok("V2 回應"); // 回傳 V2 格式
    return Ok("V1 回應");     // 預設回傳 V1 格式
}

// 策略 3:Query String 版本控制
// GET /api/products?api-version=2
[HttpGet]
public IActionResult GetAll([FromQuery(Name = "api-version")] int version = 1)
{
    return version switch
    {
        2 => Ok("V2 回應"),   // 第二版
        _ => Ok("V1 回應")    // 預設第一版
    };
}

⚠️ 錯誤回應格式:RFC 7807 Problem Details

// ASP.NET Core 內建支援 Problem Details(標準化的錯誤回應格式)
// 就像醫院的病歷表——每個欄位都有固定的格式,方便任何醫生閱讀

[HttpGet("{id}")]
public IActionResult GetById(int id)
{
    var product = _db.Products.Find(id); // 查找商品

    if (product == null) // 如果找不到
    {
        return Problem(
            detail: $"找不到 ID 為 {id} 的商品,請確認商品 ID 是否正確",  // 詳細說明
            title: "找不到資源",           // 錯誤標題
            statusCode: 404,               // HTTP 狀態碼
            instance: $"/api/products/{id}" // 發生問題的 URL
        );
        // 回傳格式:
        // {
        //   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
        //   "title": "找不到資源",
        //   "status": 404,
        //   "detail": "找不到 ID 為 5 的商品...",
        //   "instance": "/api/products/5"
        // }
    }

    return Ok(product); // 回傳 200 OK
}

// 在 Program.cs 啟用全域 Problem Details
builder.Services.AddProblemDetails(); // 註冊 Problem Details 服務

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:在 URL 中使用動詞

// ❌ 錯誤寫法:URL 用了動詞(像把說明書印在包裝上,多此一舉)
[HttpGet("api/getProducts")]       // 不要用 get 動詞
public IActionResult GetProducts() => Ok(); // GET 本身就代表「取得」

[HttpPost("api/createProduct")]    // 不要用 create 動詞
public IActionResult CreateProduct(Product p) => Ok(); // POST 就代表「建立」

[HttpPost("api/deleteProduct/{id}")] // 更不該用 POST 來刪除!
public IActionResult DeleteProduct(int id) => Ok(); // 應該用 DELETE 方法
// ✅ 正確寫法:URL 只用名詞,讓 HTTP 方法表達動作
[Route("api/products")] // 名詞、複數、小寫
[ApiController] // API 控制器
public class ProductsController : ControllerBase
{
    [HttpGet]          // GET /api/products → 取得所有
    public IActionResult GetAll() => Ok(); // HTTP 方法已經表達了「取得」

    [HttpGet("{id}")] // GET /api/products/5 → 取得單一
    public IActionResult GetById(int id) => Ok(); // 用路由參數指定資源

    [HttpPost]         // POST /api/products → 建立
    public IActionResult Create(Product p) => Created($"/api/products/{p.Id}", p);

    [HttpDelete("{id}")] // DELETE /api/products/5 → 刪除
    public IActionResult Delete(int id) => NoContent(); // 用正確的 HTTP 方法
}

解釋: HTTP 方法本身就代表動作——GET=取得、POST=建立、PUT=更新、DELETE=刪除。在 URL 裡再加上動詞就像說「我要 GET 取得(getProducts)」——重複了。URL 應該像門牌地址,只告訴你「什麼東西在哪裡」,不用告訴你「怎麼拿」。

❌ 錯誤 2:不一致的命名風格

// ❌ 錯誤寫法:整個 API 的 URL 風格不統一(像一棟大樓每層的門牌格式都不同)
[HttpGet("api/Products")]         // 大寫開頭
[HttpGet("api/user-profiles")]    // 小寫加破折號
[HttpGet("api/order_items")]      // 底線分隔
[HttpGet("api/getShippingInfo")]  // 駝峰式加動詞
// 用你 API 的開發者會崩潰!
// ✅ 正確寫法:統一使用小寫加破折號
[Route("api/products")]           // 統一風格
[Route("api/user-profiles")]      // 統一風格
[Route("api/order-items")]        // 統一風格
[Route("api/shipping-info")]      // 統一風格
// 一致的命名讓 API 更好用、更好記

解釋: API 的 URL 就像城市的路名——如果「中山路」有的地方寫「ZhongShan Road」、有的寫「zhong_shan_rd」、有的寫「中山-路」,開車的人一定會迷路。選擇一種風格,然後整個 API 都用同一種。

❌ 錯誤 3:不使用正確的 HTTP 狀態碼

// ❌ 錯誤寫法:不管發生什麼都回傳 200(像不管你問什麼,醫生都說「你很健康」)
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
    var product = _db.Products.Find(id); // 查找商品
    if (product == null)
        return Ok(new { Error = "找不到商品" }); // 明明找不到卻回 200?
    return Ok(product); // 找到了回 200
}
// ✅ 正確寫法:用正確的狀態碼告訴客戶端實際情況
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
    var product = _db.Products.Find(id); // 查找商品
    if (product == null)
        return NotFound(new { Error = $"找不到 ID={id} 的商品" }); // 404
    return Ok(product); // 200
}

// 常用狀態碼對照表:
// 200 OK              → 成功取得資料
// 201 Created         → 成功建立新資源
// 204 No Content      → 成功但沒有回傳內容(用於 PUT/DELETE)
// 400 Bad Request     → 客戶端送的資料有問題
// 401 Unauthorized    → 未登入(沒有提供身分證明)
// 403 Forbidden       → 已登入但沒有權限(有身分證但不能進 VIP 室)
// 404 Not Found       → 找不到資源
// 409 Conflict        → 資源衝突(例如重複建立)
// 500 Internal Error  → 伺服器自己出問題了

解釋: HTTP 狀態碼就像交通號誌——綠燈(200)代表通行、紅燈(4xx/5xx)代表有問題。如果不管什麼情況都亮綠燈,前端開發者就無法正確判斷結果,只能去解析回應內容才知道有沒有錯,增加了不必要的複雜度。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊