Thiết kế REST API tốt: nguyên tắc và lỗi cần tránh

Một REST API tốt là API mà người khác đoán được cách dùng mà không cần hỏi bạn. Thiết kế cẩu thả thì mỗi endpoint một kiểu, client phải tra tài liệu liên tục và mọi thay đổi đều làm vỡ ứng dụng phía dưới. Bài này trình bày các nguyên tắc cốt lõi để API dễ dùng, nhất quán và bền theo thời gian.

REST giải quyết vấn đề gì

REST là một kiểu kiến trúc do Roy Fielding mô tả, dựa trên tài nguyên (resource) và các phương thức chuẩn của HTTP. Giá trị lớn nhất của nó là tính đoán được: khi mọi endpoint tuân theo cùng quy ước, lập trình viên mới hiểu API nhanh, và công cụ chung (cache, proxy, thư viện HTTP) hoạt động đúng như mong đợi.

Đặt tên tài nguyên và URL

URL nên chỉ tài nguyên, không chỉ hành động. Dùng danh từ số nhiều, để HTTP method diễn tả hành động:

Nên Không nên
GET /users/42 GET /getUser?id=42
POST /users POST /createUser
GET /users/42/orders GET /getOrdersByUser?u=42
DELETE /orders/7 POST /deleteOrder

Quan hệ lồng nhau thể hiện qua đường dẫn: đơn hàng của một người dùng là /users/42/orders. Giữ một quy ước nhất quán (chữ thường, phân tách bằng gạch ngang) trên toàn API.

Dùng đúng HTTP method và status code

Mỗi method có ngữ nghĩa riêng, tôn trọng nó thì client và hạ tầng mới hành xử đúng:

  • GET: đọc, không được gây thay đổi dữ liệu, có thể cache.
  • POST: tạo mới hoặc thao tác không idempotent.
  • PUT: thay thế toàn bộ tài nguyên, idempotent (gọi nhiều lần cho kết quả như một lần).
  • PATCH: cập nhật một phần.
  • DELETE: xóa.

Status code phải phản ánh kết quả thật: 200/201 khi thành công, 400 khi client gửi sai, 401/403 cho xác thực và quyền, 404 khi không tìm thấy, 409 khi xung đột, 422 khi dữ liệu không hợp lệ, 500 khi lỗi server. Đừng trả 200 kèm một field “error” bên trong; như vậy client buộc phải parse body mới biết thành bại.

Định dạng phản hồi và lỗi nhất quán

Chọn một cấu trúc JSON và giữ nguyên khắp API. Với lỗi, trả về thông điệp máy đọc được: một mã lỗi ổn định, một câu mô tả cho người, và nếu là lỗi validate thì chỉ rõ trường nào sai. Client dựa vào mã lỗi để xử lý, dựa vào mô tả để hiển thị.

Phân trang, lọc và phiên bản

Endpoint trả danh sách phải có phân trang ngay từ đầu; đừng để một ngày nào đó nó trả về mười nghìn bản ghi. Đưa lọc và sắp xếp vào query string (ví dụ ?status=active&sort=-created_at) thay vì tạo endpoint riêng cho mỗi kiểu lọc.

Về phiên bản: đặt version vào URL (như /v1/users) là cách đơn giản và rõ ràng. Nguyên tắc quan trọng hơn kỹ thuật versioning là đừng phá vỡ hợp đồng cũ. Thêm field mới thường an toàn; đổi tên hay bỏ field là thay đổi phá vỡ và cần version mới.

Ví dụ thực tế

Một API cho phép “đánh dấu đơn hàng đã giao”. Thiết kế ban đầu: POST /orders/markDelivered với id trong body, luôn trả 200. Khi id không tồn tại, nó vẫn trả 200 kèm {“ok”: false}. Client không phân biệt được thành công với thất bại nếu không đọc body, và log giám sát báo mọi thứ đều xanh dù đang lỗi. Thiết kế lại theo tài nguyên: PATCH /orders/7 với body {“status”: “delivered”}. Đơn tồn tại và hợp lệ trả 200; không tồn tại trả 404; trạng thái không cho phép chuyển trả 409. Giờ client, cache và hệ thống giám sát đều hiểu đúng chuyện gì đang xảy ra mà không cần đọc kỹ body.

Sai lầm thường gặp và cách sửa

Nhét động từ vào URL. /createOrder, /updateUser phá vỡ tính nhất quán. Cách sửa: dùng danh từ cho tài nguyên, để method mang hành động.

Luôn trả 200. Che giấu lỗi khiến client và giám sát mù. Cách sửa: dùng status code phản ánh đúng kết quả.

Bỏ quên phân trang. Endpoint danh sách sẽ sập khi dữ liệu lớn. Cách sửa: phân trang từ ngày đầu.

Thông điệp lỗi không nhất quán. Mỗi endpoint một kiểu lỗi khiến client phải xử lý riêng lẻ. Cách sửa: chuẩn hóa một cấu trúc lỗi chung.

Đổi API mà không version. Đổi tên field làm vỡ mọi client cũ. Cách sửa: coi thay đổi phá vỡ là dịp ra version mới.

Checklist trước khi công bố một endpoint

  • URL chỉ tài nguyên (danh từ), không phải hành động?
  • Method HTTP đúng ngữ nghĩa (GET không đổi dữ liệu, PUT idempotent)?
  • Status code phản ánh đúng kết quả thành/bại?
  • Cấu trúc phản hồi và lỗi nhất quán với phần còn lại của API?
  • Endpoint danh sách đã có phân trang, lọc, sắp xếp?
  • Có chiến lược versioning và biết đâu là thay đổi phá vỡ?
  • Xác thực, phân quyền và giới hạn tần suất đã tính đến?

Kết luận

API tốt là sản phẩm dành cho lập trình viên khác, và tính nhất quán quan trọng hơn sự thông minh. Bước tiếp theo: rà lại các endpoint hiện có theo checklist trên, ghi ra chỗ nào lệch quy ước, và thống nhất một tài liệu quy ước ngắn cho cả đội theo. Nhất quán từ sớm rẻ hơn sửa về sau nhiều lần.

Câu hỏi thường gặp

Khi nào nên dùng PUT, khi nào PATCH?

Dùng PUT khi client gửi trọn vẹn tài nguyên để thay thế; dùng PATCH khi chỉ cập nhật vài trường. PUT idempotent, còn PATCH tùy cách bạn định nghĩa nên cần cân nhắc kỹ.

Version API trong URL hay trong header thì tốt hơn?

Cả hai đều dùng được. Version trong URL dễ nhìn, dễ test bằng trình duyệt và dễ giải thích; version qua header “sạch” hơn về lý thuyết nhưng khó quan sát. Với đa số đội, URL là lựa chọn thực dụng.

Có nhất thiết phải REST không, hay dùng GraphQL/gRPC?

Tùy nhu cầu. REST hợp với API tài nguyên công khai, tận dụng tốt cache HTTP. GraphQL mạnh khi client cần lấy dữ liệu linh hoạt; gRPC hợp giao tiếp nội bộ hiệu năng cao. Chọn theo bài toán, không theo trào lưu.

Nên xử lý lỗi validate thế nào cho gọn?

Trả một status code phù hợp (thường 400 hoặc 422) kèm danh sách các trường sai và lý do. Như vậy client hiển thị lỗi ngay tại từng ô nhập mà không phải đoán.

Nguồn tham khảo

  • Roy Fielding – luận án tiến sĩ mô tả kiến trúc REST.
  • MDN Web Docs – tài liệu về HTTP methods và status codes.

Similar Posts