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
- Stripe API Design — Gold standard
- Google API Design Guide
- Microsoft REST API Guidelines
- Designing Web APIs — Brenda Jin (O'Reilly)
- Hyrum's Law
💡 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. 📚