Mở bất kỳ codebase nào cũ hơn hai năm, bạn sẽ tìm thấy những vết sẹo: /getUser, /user_list, /Users/fetchAll, ba lược đồ phân trang khác nhau, và một trường customerID nằm cạnh order_id trong cùng một phản hồi. Không có gì trong số đó làm hỏng bất cứ điều gì. Nhưng tất cả đều làm chậm mọi người.
Việc đặt tên là quyết định thiết kế API rẻ nhất bạn từng đưa ra và cũng là quyết định tốn kém nhất để thay đổi. Một khi các client đã phụ thuộc vào /getOrders, bạn sẽ phải duy trì nó trong nhiều năm. Hướng dẫn này cung cấp cho bạn một quy tắc cụ thể cho mỗi quyết định đặt tên mà một REST API đòi hỏi, kèm theo ví dụ và phản ví dụ cho từng trường hợp. Nó tuân theo cùng tư duy với hướng dẫn REST API rộng hơn dành cho các nhà phát triển của chúng tôi, nhưng tập trung vào phần mà các nhóm thường tranh cãi nhiều nhất: đặt tên cho mọi thứ như thế nào.
Nếu bạn muốn áp dụng các quy tắc này bằng công cụ thay vì nhận xét đánh giá mã, Apidog cho phép bạn định nghĩa mọi endpoint một cách trực quan dựa trên một schema chung trước khi bất kỳ ai viết mã. Chúng tôi sẽ nói thêm về điều này ở cuối.
Sử dụng danh từ số nhiều cho các tập hợp
Một URL đặt tên cho một tài nguyên, không phải một thao tác. Các tập hợp là tập hợp của nhiều thứ, vì vậy hãy đặt tên chúng bằng danh từ số nhiều.
Nên:
GET /v1/products
GET /v1/products/89
GET /v1/orders
Không nên:
GET /v1/getProducts
GET /v1/product
GET /v1/productList
Dạng số nhiều hoạt động ở cả hai cấp độ. /products được hiểu là “tập hợp các sản phẩm” và /products/89 được hiểu là “sản phẩm số 89 trong tập hợp đó”. Việc đặt tên số ít buộc phải dùng các URL khó hiểu như /product/89 cho một mục nhưng lại là /product cho nhiều mục, điều này đọc không đúng. Hướng dẫn REST API của Microsoft đã chọn danh từ số nhiều chính vì lý do này, và hầu hết các API công khai (Stripe, GitHub, Shopify) cũng đi theo con đường tương tự.
Một ngoại lệ: các tài nguyên đơn lẻ (singleton). Nếu một người dùng chỉ có đúng một giỏ hàng, thì /users/42/cart là ổn. Đừng dùng số nhiều cho những thứ chỉ có số lượng là một.
Không dùng động từ trong đường dẫn
Phương thức HTTP chính là động từ. Việc đặt thêm một động từ khác vào đường dẫn sẽ trùng lặp thông tin và phá vỡ mô hình tài nguyên.
Nên:
GET /v1/orders/42 (đọc nó)
DELETE /v1/orders/42 (xóa nó)
PATCH /v1/orders/42 (cập nhật nó)
Không nên:
GET /v1/fetchOrder/42
POST /v1/deleteOrder/42
POST /v1/updateOrderStatus
Các đường dẫn dựa trên động từ cũng làm tăng bề mặt tiếp xúc của bạn. Một tài nguyên với bốn phương thức sẽ trở thành bốn endpoint cần tài liệu hóa, kiểm tra và lưu vào bộ nhớ cache riêng biệt. Việc vô hiệu hóa bộ nhớ cache cũng trở nên tồi tệ hơn: một CDN có thể lưu GET /v1/orders/42 vào bộ nhớ cache và vô hiệu hóa khi có DELETE /v1/orders/42 vì cả hai đều trỏ đến cùng một URL. Nó không thể kết nối /fetchOrder/42 với /deleteOrder/42.
Sử dụng kebab-case trong đường dẫn URL
Các phân đoạn đường dẫn có nhiều từ cần một ký tự phân tách, và dấu gạch nối là lựa chọn phù hợp.
Nên:
/v1/gift-cards
/v1/shipping-addresses
Không nên:
/v1/giftCards
/v1/gift_cards
/v1/GiftCards
Có ba lý do. Google coi dấu gạch nối là dấu phân cách từ để lập chỉ mục, vì vậy tài liệu API công khai xếp hạng tốt hơn với kebab-case. Dấu gạch dưới (underscores) sẽ biến mất khi một URL được gạch chân trong email hoặc tài liệu. Và camelCase trong URL dễ gây ra lỗi nhạy cảm với chữ hoa/thường: /giftCards và /giftcards là các URL khác nhau trên hầu hết các máy chủ, và ai đó sẽ gõ sai. Hướng dẫn RESTful API của Zalando coi kebab-case là một quy tắc BẮT BUỘC, và họ đã áp dụng quy tắc này cho hàng trăm dịch vụ nội bộ.
Chọn một kiểu chữ viết cho JSON và ghi lại
Đối với tên trường bên trong nội dung yêu cầu và phản hồi, câu trả lời thành thật là: camelCase và snake_case đều hoạt động. Điều không hiệu quả là trộn lẫn chúng.
Nên (một trong hai, nhất quán):
{ "orderId": 42, "createdAt": "2026-08-30T09:15:00Z", "totalAmount": 4999 }
{ "order_id": 42, "created_at": "2026-08-30T09:15:00Z", "total_amount": 4999 }
Không nên:
{ "orderId": 42, "created_at": "2026-08-30T09:15:00Z", "TotalAmount": 4999 }
camelCase dễ dàng ánh xạ sang các client JavaScript và Java. snake_case dễ đọc hơn và phù hợp với Ruby, Python, cùng với hầu hết các tên cột SQL; Stripe sử dụng nó ở khắp mọi nơi. Hãy chọn dựa trên đối tượng sử dụng API của bạn nhiều nhất, sau đó ghi lựa chọn đó vào hướng dẫn phong cách của bạn để cuộc tranh luận chỉ diễn ra một lần thay vì mỗi khi có pull request. Việc trộn lẫn kiểu chữ là điểm không nhất quán phổ biến nhất trong các API thực tế vì các nhóm khác nhau triển khai các endpoint khác nhau. Đó là một thất bại trong quản trị, chứ không phải thất bại về sở thích.
Giới hạn lồng ghép ở hai cấp độ
Lồng ghép thể hiện quyền sở hữu: /users/42/orders có nghĩa là “các đơn hàng thuộc về người dùng 42”. Điều đó hữu ích. Nhưng vượt quá hai cấp độ thì nó không còn hữu ích nữa.
Nên:
GET /v1/users/42/orders
GET /v1/orders/1337/refunds
Không nên:
GET /v1/users/42/orders/1337/refunds/7/status
Việc lồng ghép sâu buộc các client phải mang theo mọi ID của các tài nguyên cha để tiếp cận một tài nguyên con, ngay cả khi tài nguyên con đó đã có ID duy nhất của riêng nó. Nếu một khoản hoàn tiền có ID 7, hãy hiển thị nó tại /refunds/7 hoặc /orders/1337/refunds/7 và dừng lại ở đó. Một mẹo hay để kiểm tra: nếu một URL chứa ba ID trở lên, hãy làm phẳng nó. Một khi một đơn hàng đã tồn tại, nó không cần ID người dùng trong đường dẫn nữa; /orders/1337 có thể đứng độc lập.
Đặt lọc, sắp xếp và phân trang vào tham số truy vấn
Đường dẫn xác định tài nguyên. Tham số truy vấn sửa đổi cách bạn xem chúng. Không bao giờ mã hóa bộ lọc vào đường dẫn.
Nên:
GET /v1/orders?status=active&sort=-created_at&limit=50&cursor=eyJpZCI6NDJ9
GET /v1/products?category=electronics&min_price=1000
Không nên:
GET /v1/orders/active
GET /v1/orders/sorted-by-date-desc
GET /v1/getOrdersByStatusAndDate
Mẫu sort=-created_at (dấu trừ phía trước cho thứ tự giảm dần) đến từ đặc tả JSON:API và giúp bạn tiết kiệm một tham số order=desc thứ hai. Các đường dẫn lọc như /orders/active có vẻ vô hại cho đến khi bạn cần kết hợp nhiều bộ lọc, và sau đó bạn sẽ phải tạo ra một endpoint mới cho mỗi sự kết hợp. Tên tham số phân trang cũng cần sự nhất quán tương tự: hãy chọn limit/cursor hoặc page/per_page một lần và sử dụng lại chúng trên mọi tập hợp. Hướng dẫn phân trang API của chúng tôi đi sâu vào sự đánh đổi giữa cursor và offset; quy tắc đặt tên ở đây đơn giản là phải đồng nhất.
Phiên bản trong đường dẫn
Bạn có hai lựa chọn phổ biến: một phân đoạn đường dẫn (/v1/products) hoặc một header (Accept: application/vnd.myapi.v1+json). Phiên bản hóa bằng header “thuần” REST hơn, vì URL vẫn giữ nguyên tên tài nguyên qua các phiên bản, và hướng dẫn thiết kế API của Google cũng ghi nhận cả hai phương pháp đều tồn tại. Nhưng phiên bản hóa bằng đường dẫn thắng thế về mặt vận hành: nó hiển thị trong mọi dòng nhật ký, có thể kiểm tra từ trình duyệt, có thể lưu vào bộ nhớ cache mà không cần các thao tác Vary phức tạp, và client không thể quên được. Mọi nhà phát triển từng gỡ lỗi vấn đề “hoạt động trong curl, lỗi trong môi trường sản xuất” do thiếu header phiên bản đều biết cái giá phải trả của lựa chọn thay thế. Chỉ sử dụng /v1/ với phiên bản chính, không dùng /v1.2/; các thay đổi nhỏ nên là bổ sung và không gây lỗi. Để xem cây quyết định đầy đủ, bao gồm đàm phán nội dung, hãy xem so sánh của chúng tôi về các chiến lược quản lý phiên bản API.
Xử lý ID tài nguyên như một giá trị mờ, và không để lộ các số nguyên tuần tự một cách bất cẩn
/orders/41, /orders/42, /orders/43: các ID số nguyên tuần tự cho bất kỳ ai nhìn thấy biết chính xác số lượng đơn hàng bạn xử lý, và chúng mời gọi các cuộc tấn công liệt kê (enumeration attacks), nơi kẻ tấn công duyệt qua không gian ID để tìm kiếm các lỗ hổng ủy quyền. Lớp lỗi này, ủy quyền cấp đối tượng bị hỏng (broken object level authorization), đứng đầu trong OWASP API Security Top 10.
Nên:
GET /v1/orders/ord_9f8e2a71b3
GET /v1/users/550e8400-e29b-41d4-a716-446655440000
Không nên (khi việc liệt kê quan trọng):
GET /v1/orders/42
GET /v1/invoices/10883
Các ID ngẫu nhiên có tiền tố như ord_9f8e2a71b3 của Stripe là mẫu mạnh nhất: không thể đoán được, tự mô tả trong nhật ký và an toàn khi tiết lộ. Kiểm tra ủy quyền vẫn là bắt buộc trong mọi trường hợp. Các ID mờ (opaque IDs) giảm thiểu phạm vi ảnh hưởng của việc thiếu kiểm tra; chúng không thay thế việc kiểm tra đó. Nội bộ, bạn có thể giữ các khóa chính là số nguyên; quy tắc này chỉ áp dụng cho những gì bạn tiết lộ trong URL.
Mô hình hóa các hành động không phải CRUD thành tài nguyên điều khiển
Sớm hay muộn, bạn sẽ cần một hành động không có ánh xạ CRUD rõ ràng: hủy đơn hàng, thử lại thanh toán, gửi lại email. Đừng 'đào hầm' nó thông qua PATCH trên một trường trạng thái, và đừng đặt một động từ ở cấp cao nhất.
Nên:
POST /v1/orders/42/cancel
POST /v1/payments/pay_88a1/retry
Không nên:
PATCH /v1/orders/42 { "status": "cancelled" }
POST /v1/cancelOrder { "orderId": 42 }
Đây là mẫu controller, và nó là ngoại lệ được cho phép duy nhất đối với quy tắc không dùng động từ: động từ nằm ở cuối đường dẫn, được giới hạn trong tài nguyên mà nó tác động. Cách tiếp cận PATCH trông có vẻ RESTful nhưng lại che giấu một máy trạng thái bên trong một cập nhật trường. Việc hủy một đơn hàng kích hoạt hoàn tiền, giải phóng hàng tồn kho và gửi thông báo; việc giả vờ đó chỉ là một ghi trường buộc máy chủ của bạn phải so sánh các payload để phát hiện ý định. Một endpoint /cancel thể hiện rõ ý định, cấp cho hành động các quyền và nhật ký kiểm tra riêng, đồng thời tạo không gian cho các đầu vào cụ thể theo hành động như lý do hủy.
Giữ kiểu chữ nhất quán cho headers và tham số truy vấn
Hai bề mặt nhỏ hơn, cùng nguyên tắc. Các header tùy chỉnh sử dụng Hyphenated-Pascal-Case, phù hợp với quy ước HTTP: Idempotency-Key, Request-Id. Bỏ qua tiền tố X- cũ; nó đã bị phản đối bởi RFC 6648 vào năm 2012. Tên header không phân biệt chữ hoa/thường khi truyền tải, nhưng tài liệu và SDK của bạn vẫn nên viết chúng theo một cách.
Các tham số truy vấn nên khớp với kiểu chữ của nội dung JSON của bạn. Nếu nội dung của bạn sử dụng snake_case, hãy viết ?min_price=1000&created_after=2026-01-01, chứ không phải ?minPrice=1000. Một nhà phát triển đọc created_at trong phản hồi và phải gõ createdAfter trong truy vấn sẽ mắc lỗi ngay lần đầu tiên, và mọi người sau họ cũng vậy.
Tóm tắt toàn bộ bộ quy tắc
| # | Quy tắc | Nên | Không nên |
|---|---|---|---|
| 1 | Danh từ số nhiều cho các tập hợp | /products, /products/89 |
/getProducts, /productList |
| 2 | Không dùng động từ trong đường dẫn | DELETE /orders/42 |
POST /deleteOrder/42 |
| 3 | Phân đoạn đường dẫn kebab-case | /gift-cards |
/giftCards, /gift_cards |
| 4 | Một kiểu chữ JSON, có tài liệu | order_id ở khắp mọi nơi |
orderId và order_id trộn lẫn |
| 5 | Tối đa hai cấp độ lồng ghép | /orders/1337/refunds |
/users/42/orders/1337/refunds/7 |
| 6 | Bộ lọc và phân trang trong tham số truy vấn | ?status=active&sort=-created_at |
/orders/active |
| 7 | Phiên bản chính trong đường dẫn | /v1/products |
/v1.2/products, header phiên bản |
| 8 | ID tài nguyên mờ | /orders/ord_9f8e2a71b3 |
/orders/42 (công khai, có thể liệt kê) |
| 9 | Mẫu controller cho các hành động | POST /orders/42/cancel |
PATCH với {"status":"cancelled"} |
| 10 | Kiểu chữ nhất quán cho header và tham số | Idempotency-Key, ?min_price= |
X-IDEMPOTENCY_KEY, ?minPrice= trộn lẫn |
Áp dụng các quy ước ở quy mô lớn
Một hướng dẫn phong cách trên wiki không thay đổi được gì. Các nhóm có API duy trì tính nhất quán đều có một thói quen chung: họ thiết kế trước và áp dụng các quy ước trước khi mã được viết, đây là cốt lõi của quản trị API trên thực tế.
Đây là nơi Apidog khẳng định vị trí của mình trong quy trình làm việc. Các endpoint được định nghĩa trong một công cụ thiết kế trực quan theo kiểu schema-first, vì vậy đường dẫn, kiểu chữ và tên tham số là các thành phần thiết kế rõ ràng thay vì các chuỗi ẩn trong mã controller. Các thành phần dùng chung có nghĩa là các schema Pagination, Error và Money được định nghĩa một lần và tái sử dụng trên mọi endpoint; không ai phát minh lại per_page thành pageSize trên một dịch vụ mới. Và vì các thiết kế nằm trong không gian làm việc của nhóm với tính năng xem xét tích hợp sẵn, người quản lý có thể phát hiện /getUserOrders ngay từ giai đoạn thiết kế, khi việc đổi tên chỉ tốn một cú nhấp chuột, thay vì sau khi ba client đã tích hợp với nó. Đặc tả sau đó sẽ thúc đẩy việc tạo tài liệu, máy chủ mock và kiểm thử, vì vậy những tên bạn đã duyệt là những tên mà mọi người sẽ triển khai. Tải xuống Apidog và dùng thử miễn phí với endpoint mới tiếp theo của bạn; việc nâng cấp một API cũ là khó khăn, nhưng duy trì nhất quán cho các API mới thì không.
Câu hỏi thường gặp
URL REST nên là số nhiều hay số ít?
Số nhiều, đối với bất kỳ tài nguyên nào có nhiều hơn một thể hiện: /products, /orders, /users. Dạng số nhiều vẫn tự nhiên cho cả tập hợp (/orders) và một thành phần (/orders/42). Dành tên số ít cho các singleton thực sự như /users/42/cart. Nếu bạn muốn tìm hiểu sâu hơn về lý do đằng sau mô hình hóa tài nguyên, hướng dẫn của chúng tôi về REST API là gì sẽ giải thích từ các nguyên tắc cơ bản.
camelCase hay snake_case tốt hơn cho tên trường JSON?
Không có kiểu nào thắng thế về mặt giá trị. camelCase phù hợp với người dùng chủ yếu dùng JavaScript; snake_case dễ đọc hơn và khớp với Python, Ruby và API công khai của Stripe. Quy tắc cốt lõi: chọn một, ghi nó vào hướng dẫn phong cách của bạn và thực thi nó trong quá trình xem xét schema. Việc trộn lẫn kiểu chữ giữa các endpoint gây hại nhiều hơn bất kỳ lựa chọn nào.
Tôi nên đặt phiên bản API trong URL hay trong header?
Sử dụng đường dẫn (/v1/orders) trừ khi bạn có yêu cầu mạnh mẽ về hypermedia. Các phiên bản trong đường dẫn hiển thị trong nhật ký, bộ nhớ cache và các kiểm thử trình duyệt mà client không cần nỗ lực. Phiên bản hóa bằng header giữ cho URL ổn định qua các phiên bản nhưng sẽ thất bại lặng lẽ khi client quên header. Chỉ sử dụng các phiên bản chính; triển khai các thay đổi nhỏ dưới dạng các bản cập nhật bổ sung, không gây lỗi.
Có bao giờ được phép dùng động từ trong đường dẫn REST API không?
Có, ở một nơi: các endpoint controller cho các hành động không phải CRUD, như POST /orders/42/cancel hoặc POST /payments/pay_88a1/retry. Động từ nằm ở cuối đường dẫn, được giới hạn trong tài nguyên của nó, và phương thức luôn là POST. Ở mọi nơi khác, phương thức HTTP mang động từ và đường dẫn chỉ giữ các danh từ.
