🌐 Network✍️ Khoa📅 19/04/2026☕ 7 phút đọc

API Design Advanced — Thiết Kế API Mà 100 Teams Đều Dùng Được

Hyrum's Law: "Với đủ số lượng users, mọi observable behavior của API sẽ được ai đó depend vào." Kể cả cái bug. Kể cả thứ tự trả về của JSON fields. Kể cả nội dung error message.

API tốt = invisible. Không ai khen "API này thiết kế đẹp quá." Nhưng API tệ = MỌI NGƯỜI than. "Cái endpoint này return gì vậy trời?"


1. API Design Principles

1.1 Resource-Oriented Design

REST là về RESOURCES, không phải actions:

❌ Sai:
  POST /createUser
  POST /getUserById
  POST /updateUserEmail
  POST /deleteUser

✅ Đúng:
  POST   /users          → Create user
  GET    /users/{id}     → Get user
  PATCH  /users/{id}     → Update user
  DELETE /users/{id}     → Delete user
  GET    /users          → List users

Sub-resources:
  GET    /users/{id}/orders          → User's orders
  POST   /users/{id}/orders          → Create order for user
  GET    /users/{id}/orders/{orderId} → Specific order

Naming conventions:
  → Plural nouns: /users, /orders, /products
  → Lowercase, hyphen-separated: /order-items
  → Không dùng verbs trong URL (trừ RPC-style operations)

1.2 Khi REST không đủ — RPC-style Operations

Có những operations không map vào CRUD:

  POST /orders/{id}/cancel     → Cancel order
  POST /payments/{id}/refund   → Refund payment
  POST /users/{id}/verify      → Verify email

  Hoặc dùng pattern:
  POST /orders/{id}/actions/cancel  → Rõ ràng hơn

Đừng ép mọi thứ vào REST:
  → "PATCH /orders/{id} với status=cancelled" ← awkward
  → "POST /orders/{id}/cancel" ← natural, clear intent

2. Versioning Strategies

2.1 Options

1. URL Versioning (phổ biến nhất):
   /v1/users, /v2/users
   
   ✅ Đơn giản, rõ ràng, dễ debug
   ❌ URL "xấu", hard to maintain multiple versions
   Dùng bởi: Stripe, GitHub, Google

2. Header Versioning:
   Accept: application/vnd.myapi.v2+json
   
   ✅ Clean URLs
   ❌ Khó test (cần set header), less discoverable
   Dùng bởi: GitHub (alternative)

3. Query Parameter:
   /users?version=2
   
   ✅ Đơn giản
   ❌ Dễ quên, pollute query string
   Ít dùng trong practice

Recommend: URL versioning (/v1/). Đơn giản thắng.

2.2 Versioning Policy

Khi nào bump major version:
  → Remove field từ response
  → Rename field
  → Đổi type của field (string → int)
  → Đổi behavior (status code, error format)
  → Đổi authentication method

Khi nào KHÔNG cần new version:
  → Thêm field mới vào response (additive)
  → Thêm optional field vào request
  → Thêm endpoint mới
  → Fix bug (đúng spec, chỉ wrong implementation)

Golden rule: Additive changes = backward compatible = no new version

3. Backward & Forward Compatibility

3.1 Hyrum's Law in Practice

// Hyrum's Law example:
// API trả về: {"users": [{"id": 1, "name": "Khoa"}, ...]}
// Sorted by ID ascending (không document, chỉ tình cờ)

// 6 tháng sau, client code:
users := api.GetUsers()
sort.Slice(users, func(i, j int) bool { return users[i].ID < users[j].ID })
// Client bỏ sort vì "API luôn trả sorted rồi"

// Bạn optimize API, dùng concurrent fetch:
// → Response order thay đổi → Client break!

// Bài học: Document ordering guarantee hoặc KHÔNG guarantee.
// Nếu không guarantee → client PHẢI tự sort.

3.2 Robustness Principle (Postel's Law)

"Be conservative in what you send, liberal in what you accept."

Server:
  → Accept unknown fields (ignore gracefully)
  → Return well-structured, documented responses
  → Never remove fields without deprecation

Client:
  → Ignore unknown fields in response
  → Don't depend on field order
  → Handle new enum values gracefully (default case)
// ✅ Client-side: handle unknown fields
type OrderResponse struct {
    ID     string `json:"id"`
    Status string `json:"status"`
    // Nếu server thêm field mới → Go unmarshal bỏ qua → OK
}

// ✅ Client-side: handle new enum values
switch order.Status {
case "pending", "processing", "completed", "cancelled":
    handleKnownStatus(order)
default:
    // Server thêm status mới → đừng crash!
    log.Warn("Unknown order status", "status", order.Status)
    handleUnknownStatus(order)
}

4. REST vs gRPC vs GraphQL — Decision Matrix

┌─────────────┬──────────────┬──────────────┬──────────────┐
│             │ REST         │ gRPC         │ GraphQL      │
├─────────────┼──────────────┼──────────────┼──────────────┤
│ Format      │ JSON         │ Protobuf     │ JSON         │
│ Protocol    │ HTTP/1.1     │ HTTP/2       │ HTTP         │
│ Schema      │ OpenAPI      │ .proto files │ SDL          │
│ Performance │ Good         │ Excellent    │ Variable     │
│ Streaming   │ SSE/WebSocket│ Bi-directional│ Subscriptions│
│ Browser     │ Native       │ grpc-web     │ Native       │
│ Learning    │ Low          │ Medium       │ Medium-High  │
│ Tooling     │ Excellent    │ Good         │ Good         │
├─────────────┼──────────────┼──────────────┼──────────────┤
│ Best for    │ Public APIs  │ Internal svcs│ Mobile/BFF   │
│             │ Web clients  │ Low latency  │ Flexible UI  │
│             │ Simple CRUD  │ Streaming    │ Multiple     │
│             │              │ Polyglot     │ consumers    │
└─────────────┴──────────────┴──────────────┴──────────────┘

Practical recommendation:
  → Public API: REST (ubiquitous, cacheable)
  → Service-to-service: gRPC (performance, type-safe)
  → Mobile BFF: GraphQL (reduce over-fetching)
  → Internal + simple: REST (đủ tốt, team familiar)

5. API Gateway Patterns

┌──────────┐
│ Clients  │
└────┬─────┘
     │
┌────▼──────────────────────────────┐
│          API Gateway              │
│  ┌─────────────────────────────┐  │
│  │ Rate Limiting              │  │
│  │ Authentication             │  │
│  │ Request/Response Transform │  │
│  │ Routing                    │  │
│  │ Load Balancing             │  │
│  │ Caching                    │  │
│  │ Circuit Breaker            │  │
│  └─────────────────────────────┘  │
└────┬──────────┬──────────┬────────┘
     │          │          │
  ┌──▼──┐  ┌───▼──┐  ┌───▼──┐
  │Svc A│  │Svc B │  │Svc C │
  └─────┘  └──────┘  └──────┘

Tools:
  Kong: Plugin-based, Lua, open source
  AWS API Gateway: Serverless, managed
  Envoy: Cloud-native, high performance
  Traefik: Auto-discovery, Docker/K8s native

6. Idempotency Design

Idempotent = Gọi 1 lần hay N lần đều cho kết quả giống nhau.

GET, PUT, DELETE: Naturally idempotent
POST: KHÔNG idempotent (cần idempotency key)

Pattern:
  Client gửi: POST /orders
  Header: Idempotency-Key: uuid-abc-123

  Server:
    1. Check idempotency key trong cache/DB
    2. Nếu đã thấy → return cached response
    3. Nếu chưa → process, store result, return
func CreateOrder(w http.ResponseWriter, r *http.Request) {
    idempotencyKey := r.Header.Get("Idempotency-Key")
    if idempotencyKey == "" {
        http.Error(w, "Idempotency-Key required", 400)
        return
    }
    
    // Check cache
    if cached, ok := idempotencyStore.Get(idempotencyKey); ok {
        w.WriteHeader(cached.StatusCode)
        json.NewEncoder(w).Encode(cached.Body)
        return // Trả lại kết quả cũ, không process lại
    }
    
    // Process order...
    order := processOrder(r)
    
    // Cache response (TTL 24h)
    idempotencyStore.Set(idempotencyKey, Response{
        StatusCode: 201,
        Body:       order,
    }, 24*time.Hour)
    
    w.WriteHeader(201)
    json.NewEncoder(w).Encode(order)
}

7. Deprecation & Sunset Strategy

Lifecycle:
  STABLE → DEPRECATED → SUNSET → REMOVED

Timeline (best practice):
  DEPRECATED: Announce, add headers, log usage
    → Duration: ≥ 6 months
    → Headers:
      Deprecation: true
      Sunset: Sat, 01 Mar 2025 00:00:00 GMT
      Link: <https://docs.api.com/migration>; rel="successor"

  SUNSET: Throttle, warn aggressively
    → Duration: 3 months
    → Reduce rate limit
    → Return warning in response body
    → Contact remaining consumers directly

  REMOVED: 
    → Return 410 Gone + migration instructions
    → After 1 month: 404

Communication:
  → Email/Slack notification to API consumers
  → Changelog/release notes
  → Migration guide with code examples
  → Office hours for questions

8. Webhook Design

// Webhook = Server-to-server push notification

// Design principles:
// 1. Retry with exponential backoff
// 2. Signature verification (HMAC-SHA256)
// 3. Idempotent handling (event_id)
// 4. Payload chứa enough info hoặc link to fetch

// Webhook payload:
{
    "event_id": "evt_abc123",       // Idempotency
    "event_type": "order.completed",
    "created_at": "2024-03-15T10:00:00Z",
    "data": {
        "order_id": "ord_xyz",
        "total": 50000,
        "currency": "VND"
    },
    "webhook_url": "https://customer.com/webhooks"
}

// Signature verification (receiver side):
func verifyWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

// Retry policy:
// Attempt 1: Immediately
// Attempt 2: 1 minute
// Attempt 3: 5 minutes
// Attempt 4: 30 minutes
// Attempt 5: 2 hours
// Attempt 6: 24 hours
// After 6 fails: disable webhook, notify customer

9. Error Response Design

// ✅ Consistent error format:
{
    "error": {
        "code": "INSUFFICIENT_FUNDS",
        "message": "Account balance is insufficient for this transaction",
        "details": [
            {
                "field": "amount",
                "message": "Required: 50000 VND, Available: 30000 VND"
            }
        ],
        "request_id": "req_abc123",
        "documentation_url": "https://docs.api.com/errors#insufficient-funds"
    }
}

// Error codes (machine-readable, stable):
// → INSUFFICIENT_FUNDS, INVALID_PARAMETER, RESOURCE_NOT_FOUND
// → Clients switch on code, NOT on message

// Error messages (human-readable, can change):
// → "Account balance is insufficient"
// → Safe to improve wording without breaking clients

Tài liệu tham khảo


💡 Remember: API tốt nhất là API mà developer dùng xong mà không cần đọc docs. Nhưng vẫn phải viết docs. 📚