Các Lỗi Thiết kế API Thường Gặp và Cách Tránh
Thiết kế API sạch, dự đoán được là rất quan trọng cho khả năng bảo trì, trải nghiệm khách hàng và sự phát triển lâu dài của backend. Hướng dẫn này làm nổi bật các lỗi thiết kế API thường gặp và cung cấp mẫu thực tế để tránh chúng, tập trung vào:
- Quy tắc đặt tên
- Phiên bản
- Tính idempotency
- Bẫy phân trang
- Cấu trúc phản hồi không nhất quán
1. Quy tắc Đặt tên: Giữ Nhất quán và Dựa trên Tài nguyên
1.1 Lỗi: Endpoint dựa trên Động từ, Không nhất quán
GET /getUsers
POST /createUser
DELETE /removeUserByIdVấn đề:
- Lẫn lộn giữa động từ (
get,create,remove) và danh từ (Users,User) - Khó đoán endpoint mới
- Không tuân theo quy ước RESTful
1.2 Khuyến nghị: Dựa trên Danh từ, Hướng về Tài nguyên
Sử dụng danh từ số nhiều cho bộ sưu tập và mẫu nhất quán:
GET /users # danh sách users
POST /users # tạo user
GET /users/{id} # lấy user theo id
PATCH /users/{id} # cập nhật một phần user
PUT /users/{id} # thay thế user
DELETE /users/{id} # xóa userHướng dẫn Đặt tên
- Sử dụng lowercase-kebab-case cho đường dẫn:
/user-profiles,/access-tokens - Ưu tiên danh từ (tài nguyên) thay vì động từ (hành động)
- Giữ tên tài nguyên nhất quán giữa các dịch vụ:
users,orders,payments - Sử dụng tài nguyên con cho quan hệ phân cấp:
/users/{id}/orders
1.3 Lỗi: Mã hóa Hành động trong Đường dẫn Thay vì Phương thức
POST /users/123/activate
POST /users/123/deactivateĐôi khi hành động là cần thiết, nhưng lạm dụng chúng dẫn đến API kiểu RPC.
Tốt hơn:
POST /users/123/activation # tạo tài nguyên activation
DELETE /users/123/activation # xóa activationHoặc sử dụng endpoint kiểu lệnh chỉ khi thực sự cần:
POST /users/123:activate2. Phiên bản: Tránh Phá vỡ Client Một cách Thầm lặng
2.1 Lỗi: Không có Phiên bản
Thay đổi phản hồi hoặc hành vi mà không có chiến lược phiên bản sẽ phá vỡ client hiện có một cách bất ngờ.
GET /usersTại t0: trả về { id, name } Tại t1: đột nhiên trả về { id, fullName, status }
2.2 Khuyến nghị: Chiến lược Phiên bản Riêng biệt
Các phương pháp phổ biến nhất:
- Phiên bản URL (đơn giản, rõ ràng):
GET /v1/users
GET /v2/users- Phiên bản dựa trên Header (URL sạch):
GET /users
Accept: application/vnd.myapp.v1+jsonLời khuyên Thực tế
- Bắt đầu với phiên bản URL để đơn giản:
/api/v1/... - Coi thay đổi phá vỡ là phiên bản chính mới
- Tránh tạo phiên bản mới cho thay đổi không phá vỡ (thêm trường)
2.3 Lỗi: Phiên bản Không nhất quán trong Cùng một API
GET /api/v1/users
GET /api/orders # không có phiên bảnGiữ phiên bản nhất quán giữa tất cả tài nguyên của cùng một API.
3. Idempotency: Retry An toàn mà Không có Tác dụng Phụ
3.1 Lỗi: Thao tác Không idempotent Không có Bảo vệ
POST /payments
Body: { "orderId": "123", "amount": 100 }Nếu client retry yêu cầu do timeout, bạn có thể thu phí hai lần.
3.2 Khuyến nghị: Sử dụng Idempotency Key cho Thao tác Nhạy cảm
Sử dụng header như Idempotency-Key để xác định duy nhất một thao tác client:
POST /payments
Idempotency-Key: 7b8e9b74-0abc-4f52-9c8a-91e4c4d27e8c
Body: { "orderId": "123", "amount": 100 }Ở phía server:
- Kiểm tra xem
Idempotency-Keynày đã được xem chưa. - Nếu có, trả về kết quả giống nhau như lần gọi trước.
- Nếu không, xử lý và lưu kết quả theo idempotency key.
3.3 Phương thức Idempotent vs Không idempotent
Ngữ nghĩa HTTP
- An toàn (không thay đổi trạng thái):
GET,HEAD - Idempotent:
PUT,DELETE,PATCH(nên được thiết kế để idempotent) - Không idempotent mặc định:
POST
Lỗi: Lạm dụng POST cho Cập nhật Idempotent
POST /users/123Tốt hơn:
PUT /users/123 # thay thế user (idempotent)
PATCH /users/123 # cập nhật một phần; thiết kế để idempotent4. Bẫy Phân trang: Vấn đề Hiệu suất và UX
4.1 Lỗi: Chỉ Phân trang Dựa trên Offset
GET /users?offset=0&limit=20
GET /users?offset=20&limit=20Vấn đề:
- Truy vấn tốn kém trên tập dữ liệu lớn (
OFFSETtrong SQL) - Kết quả không nhất quán nếu dữ liệu mới được chèn giữa các yêu cầu
4.2 Khuyến nghị: Phân trang Dựa trên Cursor cho Danh sách Lớn
GET /users?limit=20
GET /users?limit=20&cursor=eyJpZCI6IjEyMyJ9Ví dụ phản hồi:
{
"data": [
{ "id": "120", "name": "Alice" },
{ "id": "121", "name": "Bob" }
],
"pagination": {
"nextCursor": "eyJpZCI6IjEyMSJ9",
"hasNextPage": true
}
}Hướng dẫn Phân trang
- Luôn trả về metadata:
total,limit,cursor/offset,hasNextPage - Giữ phân trang nhất quán giữa tất cả endpoint danh sách
- Cho API admin/nội bộ, offset có thể chấp nhận được; cho API user-facing, ưu tiên dựa trên cursor
4.3 Lỗi: Tên Tham số Không nhất quán
GET /users?page=1&size=20
GET /orders?offset=0&limit=20Chọn một tiêu chuẩn và tuân theo nó:
GET /users?page=1&pageSize=20
GET /orders?page=1&pageSize=20Hoặc:
GET /users?limit=20&cursor=...5. Cấu trúc Phản hồi Không nhất quán
5.1 Lỗi: Hình thức Khác nhau cho Mỗi Endpoint
// /users/123
{
"id": "123",
"name": "Alice"
}
// /orders/456
{
"order": {
"id": "456",
"total": 100
},
"status": "OK"
}Client phải xử lý nhiều định dạng, tăng độ phức tạp và lỗi.
5.2 Khuyến nghị: Envelope Phản hồi Tiêu chuẩn
Định nghĩa cấu trúc phản hồi chung:
// Thành công
{
"success": true,
"data": {
"id": "123",
"name": "Alice"
},
"error": null,
"meta": {
"requestId": "abc-123",
"timestamp": "2025-01-01T10:00:00Z"
}
}// Lỗi
{
"success": false,
"data": null,
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"details": null
},
"meta": {
"requestId": "abc-123",
"timestamp": "2025-01-01T10:00:01Z"
}
}Lợi ích của Envelope Tiêu chuẩn
- Xử lý lỗi client-side dễ hơn
- Logging và tracing nhất quán (
requestId,timestamp) - Làm cho metadata phân trang, lọc, và sắp xếp có thể dự đoán được
5.3 Lỗi: Bỏ qua Mã Trạng thái HTTP
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": false,
"error": "User not found"
}Điều này buộc client phải kiểm tra body để phát hiện lỗi.
Tốt hơn:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}6. Ví dụ: Kết hợp Tất cả
6.1 API User Được thiết kế Tốt
GET /api/v1/users?page=1&pageSize=20
GET /api/v1/users/{id}
POST /api/v1/users
PATCH /api/v1/users/{id}
DELETE /api/v1/users/{id}Phản hồi Danh sách:
{
"success": true,
"data": [
{ "id": "123", "name": "Alice" },
{ "id": "124", "name": "Bob" }
],
"error": null,
"meta": {
"pagination": {
"page": 1,
"pageSize": 20,
"total": 52,
"totalPages": 3,
"hasNextPage": true
},
"requestId": "req-123"
}
}6.2 Tạo Thanh toán Idempotent
POST /api/v1/payments
Idempotency-Key: 7b8e9b74-0abc-4f52-9c8a-91e4c4d27e8c
{
"orderId": "123",
"amount": 100.0,
"currency": "USD"
}{
"success": true,
"data": {
"id": "pay_001",
"orderId": "123",
"status": "COMPLETED"
},
"error": null,
"meta": {
"requestId": "req-xyz",
"idempotencyKey": "7b8e9b74-0abc-4f52-9c8a-91e4c4d27e8c"
}
}7. Checklist Thực hành Tốt nhất
Checklist Thiết kế API
- Đặt tên
- Sử dụng danh từ số nhiều và URL hướng về tài nguyên
- Giữ kebab-case cho đường dẫn và tên nhất quán
- Sử dụng tài nguyên con cho các mối quan hệ (
/users/{id}/orders)
- Phiên bản
- Bắt đầu với phiên bản dựa trên URL (
/api/v1/...) - Chỉ tăng phiên bản chính cho thay đổi phá vỡ
- Bắt đầu với phiên bản dựa trên URL (
- Idempotency
- Sử dụng idempotency key cho các thao tác quan trọng (thanh toán, đơn hàng)
- Thiết kế thao tác
PUT/PATCHđể idempotent
- Phân trang
- Cung cấp tham số nhất quán (
page/pageSizehoặclimit/cursor) - Bao gồm metadata phân trang trong phản hồi
- Cung cấp tham số nhất quán (
- Phản hồi
- Sử dụng envelope phản hồi tiêu chuẩn
- Sử dụng mã trạng thái HTTP thích hợp
- Bao gồm metadata:
requestId,timestamp,pagination:::
8. Kết luận
Các quyết định thiết kế API nhỏ sẽ tích lũy theo thời gian. Đặt tên không nhất quán, thiếu phiên bản, thao tác không idempotent, phân trang kém và cấu trúc phản hồi không đều đều đều tăng chi phí tích hợp và làm chậm quá trình phát triển.
Bằng cách tuân theo các quy ước trong hướng dẫn này, bạn có thể xây dựng các API:
- Dự đoán được cho client
- An toàn để phát triển
- Dễ debug và giám sát
- Thân thiện cho frontend và người tiêu dùng bên thứ ba
Bắt đầu bằng cách chuẩn hóa một khu vực (đặt tên hoặc phản hồi), sau đó dần dần áp dụng các nguyên tắc này trên tất cả các dịch vụ của bạn.
