Skip to content

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

http
GET /getUsers
POST /createUser
DELETE /removeUserById

Vấ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:

http
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 user

Hướ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

http
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:

http
POST /users/123/activation       # tạo tài nguyên activation
DELETE /users/123/activation     # xóa activation

Hoặc sử dụng endpoint kiểu lệnh chỉ khi thực sự cần:

http
POST /users/123:activate

2. 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ờ.

http
GET /users

Tạ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):
http
GET /v1/users
GET /v2/users
  • Phiên bản dựa trên Header (URL sạch):
http
GET /users
Accept: application/vnd.myapp.v1+json

Lờ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

http
GET /api/v1/users
GET /api/orders     # không có phiên bản

Giữ 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ệ

http
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:

http
POST /payments
Idempotency-Key: 7b8e9b74-0abc-4f52-9c8a-91e4c4d27e8c
Body: { "orderId": "123", "amount": 100 }

Ở phía server:

  1. Kiểm tra xem Idempotency-Key này đã được xem chưa.
  2. Nếu có, trả về kết quả giống nhau như lần gọi trước.
  3. 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

http
POST /users/123

Tốt hơn:

http
PUT /users/123      # thay thế user (idempotent)
PATCH /users/123    # cập nhật một phần; thiết kế để idempotent

4. 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

http
GET /users?offset=0&limit=20
GET /users?offset=20&limit=20

Vấn đề:

  • Truy vấn tốn kém trên tập dữ liệu lớn (OFFSET trong 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

http
GET /users?limit=20
GET /users?limit=20&cursor=eyJpZCI6IjEyMyJ9

Ví dụ phản hồi:

json
{
  "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

http
GET /users?page=1&size=20
GET /orders?offset=0&limit=20

Chọn một tiêu chuẩn và tuân theo nó:

http
GET /users?page=1&pageSize=20
GET /orders?page=1&pageSize=20

Hoặc:

http
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

json
// /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:

json
// Thành công
{
  "success": true,
  "data": {
    "id": "123",
    "name": "Alice"
  },
  "error": null,
  "meta": {
    "requestId": "abc-123",
    "timestamp": "2025-01-01T10:00:00Z"
  }
}
json
// 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
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
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

http
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:

json
{
  "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

http
POST /api/v1/payments
Idempotency-Key: 7b8e9b74-0abc-4f52-9c8a-91e4c4d27e8c

{
  "orderId": "123",
  "amount": 100.0,
  "currency": "USD"
}
json
{
  "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ỡ
  • 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/pageSize hoặc limit/cursor)
    • Bao gồm metadata phân trang trong phản hồi
  • 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ợplà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.

Được phát hành theo giấy phép MIT.