EngineeringSenior DeveloperTraining Sessions

Session 13 - API Design: Thiết Kế API Mà Consumer Muốn Dùng

Nhiều dev thiết kế API "dựa trên database schema" - mỗi table = 1 endpoint, mỗi column = 1 field. Kết quả: API khó dùng, N+1 ở client side, và consumer ghét bạn.


Mục tiêu

  • Thiết kế API từ góc nhìn consumer (không phải database)
  • RESTful đúng nghĩa (không chỉ CRUD mapping)
  • Error responses chuẩn (ProblemDetails)
  • Versioning strategy thực chiến

1) Thiết Kế Từ Consumer, Không Từ Database

csharp
// ❌ Database-driven API: mỗi table = 1 endpoint
GET /api/orders/123           → { orderId, customerId, total, status }
GET /api/customers/456        → { name, email, phone }
GET /api/order-items?orderId=123 → [{ productId, qty, price }]
GET /api/products/789         → { name, category }

// Client cần hiển thị "Order Detail" → 4 API calls! N+1 ở client side!

// ✅ Consumer-driven API: 1 endpoint = 1 use case
GET /api/orders/123
→ {
    "id": "123",
    "customer": { "name": "Thuan", "email": "..." },
    "items": [
      { "product": "Widget", "quantity": 2, "unitPrice": 15.00 }
    ],
    "total": 30.00,
    "status": "confirmed"
  }
// → 1 API call, TẤT CẢ data client cần!

Câu hỏi chính khi design endpoint

1. Consumer cần GÌ (không phải bạn CÓ gì trong DB)?
2. Bao nhiêu API calls để hoàn thành 1 UI action?
   (1 call = tốt, N calls = xem lại design)
3. Response có chứa data KHÔNG CẦN? (over-fetching)
4. Response có THIẾU data cần? (under-fetching → N+1)

2) RESTful Đúng Nghĩa

HTTP Methods - Nghĩa Thực Sự

GET:    Lấy data (SAFE, IDEMPOTENT)
        → KHÔNG thay đổi state
        → Cacheable

POST:   Tạo mới hoặc trigger action (NOT idempotent)
        → POST /api/orders → Tạo order mới (mỗi lần = order mới)
        → POST /api/reports/generate → Trigger hành động

PUT:    Replace TOÀN BỘ resource (IDEMPOTENT)
        → PUT /api/orders/123 → Replace order 123 hoàn toàn
        → Gọi 2 lần = kết quả GIỐNG NHAU

PATCH:  Update PARTIAL resource (IDEMPOTENT)
        → PATCH /api/orders/123 → Cập nhật MỘT SỐ fields

DELETE: Xóa resource (IDEMPOTENT)
        → DELETE /api/orders/123 → Xóa (gọi 2 lần = vẫn 404 hoặc 204)

Status Codes - Dùng Đúng

200 OK:         Request thành công (GET, PUT, PATCH)
201 Created:    Resource mới được tạo (POST) + Location header
204 No Content: Thành công, không có body (DELETE, PUT)
400 Bad Request: Input validation fail (client lỗi)
401 Unauthorized: Chưa đăng nhập (authentication fail)
403 Forbidden:  Đã đăng nhập nhưng KHÔNG CÓ QUYỀN
404 Not Found:  Resource không tồn tại
409 Conflict:   State conflict (e.g., đã tồn tại, concurrency)
422 Unprocessable: Business rule violation
429 Too Many Requests: Rate limited
500 Internal Server Error: Server bug (KHÔNG expose details)
csharp
// ❌ Sai status codes
return Ok(null);                    // 200 nhưng null body → dùng 404
return BadRequest("Order not found"); // 400 = client lỗi, nhưng đây là "not found" → 404
return Ok(new { success = false });  // 200 nhưng "failed" → dùng 4xx/5xx status code

// ✅ Đúng
return NotFound();                   // 404
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order); // 201 + Location
return NoContent();                  // 204 (sau DELETE hoặc PUT)

3) Error Response - ProblemDetails (RFC 7807)

csharp
// ❌ Mỗi dev trả error KHÁC FORMAT
{ "error": "Not found" }
{ "message": "Invalid email", "code": 101 }
{ "success": false, "errors": ["..."] }
// → Client phải handle N formats khác nhau!

// ✅ ProblemDetails: chuẩn industry
{
  "type": "https://httpstatuses.com/422",
  "title": "Business rule violated",
  "status": 422,
  "detail": "Cannot cancel an order that has been shipped",
  "instance": "/api/orders/123/cancel",
  "errors": {
    "status": ["Order is in 'Shipped' state"]
  }
}

// ASP.NET Core 7+: built-in ProblemDetails
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = ctx =>
    {
        ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier;
    };
});

4) Pagination, Filtering, Sorting

csharp
// ✅ Standardized query parameters
GET /api/products?page=2&pageSize=20&sort=price:desc&filter=category:electronics

// Response với pagination metadata
{
  "data": [{ ... }, { ... }],
  "pagination": {
    "page": 2,
    "pageSize": 20,
    "totalPages": 15,
    "totalItems": 287,
    "hasNext": true,
    "hasPrevious": true
  }
}

// Implementation
app.MapGet("/api/products", async (
    [AsParameters] ProductQuery query,
    AppDbContext db) =>
{
    var baseQuery = db.Products.AsNoTracking();

    // Filter
    if (query.Category is not null)
        baseQuery = baseQuery.Where(p => p.Category == query.Category);

    // Sort
    baseQuery = query.Sort switch
    {
        "price:asc" => baseQuery.OrderBy(p => p.Price),
        "price:desc" => baseQuery.OrderByDescending(p => p.Price),
        "name" => baseQuery.OrderBy(p => p.Name),
        _ => baseQuery.OrderByDescending(p => p.CreatedAt)
    };

    // Paginate
    var totalItems = await baseQuery.CountAsync();
    var items = await baseQuery
        .Skip((query.Page - 1) * query.PageSize)
        .Take(query.PageSize)
        .Select(p => new ProductDto { /* ... */ })
        .ToListAsync();

    return Results.Ok(new PagedResponse<ProductDto>(items, query.Page, query.PageSize, totalItems));
});

5) API Versioning

csharp
// Strategy 1: URL Path (phổ biến nhất, rõ ràng nhất)
// /api/v1/orders
// /api/v2/orders
app.MapGroup("/api/v1").MapOrdersV1();
app.MapGroup("/api/v2").MapOrdersV2();

// Strategy 2: Header (clean URL, nhưng khó debug)
// X-Api-Version: 2
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});

// Khi nào tăng version?
// → BREAKING CHANGE: xóa field, đổi type, đổi behavior
// → Non-breaking: thêm field (optional), thêm endpoint → KHÔNG cần version mới

Master Checklist ✅

□ API thiết kế từ consumer need, không từ DB schema?
□ 1 UI action = 1 API call (không N+1 ở client)?
□ HTTP methods đúng nghĩa (GET safe, POST/PUT/PATCH đúng semantic)?
□ Status codes chính xác (400 vs 404 vs 422)?
□ Error format chuẩn (ProblemDetails)?
□ Pagination cho list endpoints?
□ Versioning strategy rõ ràng?
□ Response KHÔNG over-fetch (DTO, không entity)?

Tư duy master: API tốt là API mà consumer đọc docs xong, KHÔNG CẦN HỎI BẠN. Nếu consumer phải slack bạn "field này nghĩa là gì?" → API design cần cải thiện.