Web API 開發
Web API 是什麼?
Web API 就像是餐廳的外送窗口——不提供堂食(HTML 頁面),只提供打包好的餐點(JSON 資料)給外送平台(前端、手機 App)。
前端 App / 手機 App
↓ HTTP Request(JSON)
[ApiController]
↓ 處理
↑ HTTP Response(JSON)
前端 App / 手機 App
[ApiController] 基礎
// Controllers/ProductsApiController.cs
[ApiController] // 標記為 API 控制器
[Route("api/[controller]")] // 路由:api/ProductsApi
public class ProductsApiController : ControllerBase // 繼承 ControllerBase(不是 Controller)
{
private readonly IProductService _service; // 商品服務
// 建構子注入
public ProductsApiController(IProductService service)
{
_service = service; // 保存服務參考
}
// GET api/ProductsApi
[HttpGet] // 處理 GET 請求
public ActionResult<List<Product>> GetAll()
{
var products = _service.GetAll(); // 取得所有商品
return Ok(products); // 回傳 200 + JSON
}
// GET api/ProductsApi/5
[HttpGet("{id}")] // 路由參數
public ActionResult<Product> GetById(int id)
{
var product = _service.GetById(id); // 用 ID 查詢
if (product == null)
return NotFound(); // 找不到回傳 404
return Ok(product); // 回傳 200 + JSON
}
// POST api/ProductsApi
[HttpPost] // 處理 POST 請求
public ActionResult<Product> Create(
[FromBody] CreateProductDto dto) // 從請求主體綁定
{
var product = _service.Create(dto); // 建立商品
return CreatedAtAction( // 回傳 201 Created
nameof(GetById), // 指向 GetById Action
new { id = product.Id }, // 路由參數
product); // 回應主體
}
// PUT api/ProductsApi/5
[HttpPut("{id}")] // 處理 PUT 請求
public IActionResult Update(
int id,
[FromBody] UpdateProductDto dto) // 從請求主體綁定
{
if (!_service.Exists(id))
return NotFound(); // 找不到回傳 404
_service.Update(id, dto); // 更新商品
return NoContent(); // 回傳 204 No Content
}
// DELETE api/ProductsApi/5
[HttpDelete("{id}")] // 處理 DELETE 請求
public IActionResult Delete(int id)
{
if (!_service.Exists(id))
return NotFound(); // 找不到回傳 404
_service.Delete(id); // 刪除商品
return NoContent(); // 回傳 204 No Content
}
}
Model Binding 模型綁定
// 各種綁定來源
[HttpGet("search")]
public IActionResult Search(
[FromQuery] string keyword, // 從 URL 查詢字串:?keyword=手機
[FromQuery] int page = 1, // 預設值為 1
[FromHeader(Name = "X-Api-Key")] string? apiKey) // 從 HTTP Header
{
return Ok(new { keyword, page, apiKey }); // 回傳綁定結果
}
[HttpPost("upload")]
public IActionResult Upload(
[FromForm] string description, // 從表單欄位
[FromForm] IFormFile file) // 從表單檔案
{
return Ok(new { description, file.FileName }); // 回傳檔案名稱
}
[HttpPut("{id}")]
public IActionResult Update(
[FromRoute] int id, // 從路由參數
[FromBody] UpdateDto dto) // 從請求主體(JSON)
{
return Ok(new { id, dto }); // 回傳更新資料
}
DTO(Data Transfer Object)
// DTOs/CreateProductDto.cs - 建立商品用的 DTO
public class CreateProductDto
{
[Required(ErrorMessage = "商品名稱必填")] // 必填驗證
[StringLength(100, ErrorMessage = "名稱最多 100 字")]
public string Name { get; set; } = ""; // 商品名稱
[Range(0, 999999, ErrorMessage = "價格必須在 0~999999")]
public decimal Price { get; set; } // 商品價格
public string? Description { get; set; } // 商品描述(可選)
}
// DTOs/ProductResponseDto.cs - 回應用的 DTO
public class ProductResponseDto
{
public int Id { get; set; } // 商品 ID
public string Name { get; set; } = ""; // 商品名稱
public decimal Price { get; set; } // 價格
// 注意:不包含敏感欄位如 Cost、Supplier 等
}
ActionResult 與 IActionResult
// ActionResult<T> → 有明確回傳型別(Swagger 能自動產生文件)
[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), 200)] // 200 回傳 Product
[ProducesResponseType(404)] // 404 找不到
public ActionResult<Product> GetById(int id)
{
var product = _service.GetById(id); // 查詢商品
if (product == null) return NotFound(); // 404
return Ok(product); // 200 + Product JSON
}
// IActionResult → 回傳型別不固定
[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); // 400 驗證失敗
var product = _service.Create(dto); // 建立商品
return CreatedAtAction( // 201 Created
nameof(GetById), new { id = product.Id }, product);
}
API 版本控制
// Program.cs - 設定 API 版本控制
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0); // 預設版本 1.0
options.AssumeDefaultVersionWhenUnspecified = true; // 未指定時用預設版本
options.ReportApiVersions = true; // 回應中回報版本資訊
});
// v1 控制器
[ApiController]
[Route("api/v{version:apiVersion}/products")] // URL 路徑版本控制
[ApiVersion("1.0")] // 版本 1.0
public class ProductsV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() =>
Ok(new { version = "v1", data = "舊格式" }); // v1 的回應格式
}
// v2 控制器
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("2.0")] // 版本 2.0
public class ProductsV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() =>
Ok(new { version = "v2", items = "新格式" }); // v2 的新回應格式
}
Swagger / OpenAPI
// Program.cs - 設定 Swagger
builder.Services.AddEndpointsApiExplorer(); // API 探索器
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo // Swagger 文件設定
{
Title = "商品 API", // API 標題
Version = "v1", // 版本號
Description = "商品管理 RESTful API" // 說明
});
});
var app = builder.Build();
// 只在開發環境啟用 Swagger UI
if (app.Environment.IsDevelopment())
{
app.UseSwagger(); // 啟用 Swagger JSON
app.UseSwaggerUI(); // 啟用 Swagger UI
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:所有情況都回傳 200
// ❌ 找不到也回傳 200(前端無法判斷是否成功)
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var product = _service.GetById(id); // 查詢商品
return Ok(product); // ❌ product 可能是 null!
}
// ✅ 正確使用 HTTP 狀態碼
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var product = _service.GetById(id); // 查詢商品
if (product == null)
return NotFound(new { message = "商品不存在" }); // 404 找不到
return Ok(product); // 200 找到了
}
為什麼? HTTP 狀態碼是 API 的通用語言,
200表示成功,404表示找不到。前端靠狀態碼判斷如何處理回應。
❌ 錯誤 2:直接回傳 Entity(沒用 DTO)
// ❌ 直接回傳資料庫實體(洩漏敏感資料)
[HttpGet("{id}")]
public ActionResult<User> GetUser(int id)
{
var user = _db.Users.Find(id); // 查詢使用者
return Ok(user); // ❌ 包含 PasswordHash!
}
// JSON 回應會包含:{ "passwordHash": "abc123...", ... }
// ✅ 使用 DTO 只回傳需要的欄位
[HttpGet("{id}")]
public ActionResult<UserDto> GetUser(int id)
{
var user = _db.Users.Find(id); // 查詢使用者
if (user == null) return NotFound(); // 找不到
var dto = new UserDto // 建立 DTO
{
Id = user.Id, // 只包含安全的欄位
Username = user.Username, // 使用者名稱
Email = user.Email // 電子郵件
};
return Ok(dto); // 回傳 DTO
}
為什麼? 資料庫實體可能包含密碼雜湊、內部 ID 等敏感資訊。DTO 只暴露前端需要的欄位,保護資料安全。
❌ 錯誤 3:API 沒有驗證輸入
// ❌ 完全不驗證就直接用(可能收到垃圾資料)
[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto)
{
_service.Create(dto); // ❌ dto 的欄位可能是 null!
return Ok();
}
// ✅ 用 DataAnnotation + ModelState 驗證
[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto)
{
if (!ModelState.IsValid) // 檢查驗證結果
return BadRequest(ModelState); // 回傳 400 + 錯誤訊息
var product = _service.Create(dto); // 驗證通過才建立
return CreatedAtAction(nameof(GetById),
new { id = product.Id }, product); // 回傳 201
}
為什麼? 永遠不要信任前端傳來的資料!DataAnnotation 加上 ModelState 驗證可以在進入商業邏輯前就擋掉不合法的輸入。