Thiết kế REST API chuẩn: Nguyên tắc, cấu trúc và Best Practices
Bùi Tấn Lực
- 105
- 05/09/2026
REST API là lớp giao tiếp quan trọng giữa website, ứng dụng di động, hệ thống quản trị và các dịch vụ phía máy chủ. Một API được thiết kế tốt không chỉ giúp client lấy và gửi dữ liệu thuận tiện mà còn quyết định khả năng mở rộng, bảo trì, bảo mật và tích hợp của toàn bộ hệ thống.
Trong thực tế, nhiều API vẫn hoạt động bình thường nhưng nhanh chóng trở nên khó phát triển khi số lượng endpoint tăng lên. Nguyên nhân thường không nằm ở ngôn ngữ lập trình mà xuất phát từ cách đặt URL, sử dụng HTTP method, tổ chức dữ liệu, xử lý lỗi và xác định ranh giới giữa các tài nguyên.
Vì vậy, thiết kế REST API chuẩn cần bắt đầu từ kiến trúc và quy ước nhất quán thay vì chỉ tạo ra những URL có thể trả về dữ liệu. Với Web Mới, việc xây dựng API được nhìn nhận theo hướng thực tế: cấu trúc phải dễ hiểu đối với người sử dụng API, đồng thời đủ chặt chẽ để hệ thống có thể phát triển lâu dài.

REST API nên được thiết kế theo tư duy tài nguyên
Điểm cốt lõi của REST nằm ở cách hệ thống biểu diễn và thao tác với resource (tài nguyên). Thay vì xem mỗi chức năng là một URL riêng biệt, API nên xác định những đối tượng mà hệ thống đang quản lý, sau đó sử dụng HTTP method để mô tả hành động cần thực hiện.
Ví dụ, một hệ thống bán hàng có thể có các tài nguyên như người dùng, sản phẩm, đơn hàng và danh mục. Khi đó, API nên tập trung vào những tài nguyên này thay vì tạo URL theo kiểu mô tả thao tác.
GET /products
GET /products/25
POST /products
PUT /products/25
DELETE /products/25
Cách tổ chức này tạo ra một quy ước dễ dự đoán. Khi lập trình viên nhìn thấy GET /products/25, họ có thể hiểu đây là yêu cầu lấy thông tin sản phẩm có mã 25 mà không cần biết cách xử lý bên trong máy chủ.
Ngược lại, việc tạo các endpoint thiên về động từ như /getProducts, /createProduct hoặc /deleteProduct khiến URL phải mô tả cả hành động trong khi HTTP method đã có nhiệm vụ thể hiện hành động đó.
REST không bắt buộc mọi API phải tuân theo một bộ quy tắc tuyệt đối. Tuy nhiên, khi đã lựa chọn cách tiếp cận RESTful, điều quan trọng là các endpoint phải nhất quán. Một API có thể dùng quy ước khác nhau vì lý do nghiệp vụ, nhưng không nên để cùng một loại tài nguyên được biểu diễn theo nhiều cách không liên quan.
Danh từ nên được ưu tiên trong endpoint
Tên tài nguyên thường nên sử dụng danh từ thay vì động từ. Đây là cách đơn giản để phân biệt phần tài nguyên với phần hành động.
GET /customers
GET /customers/15
POST /customers
PATCH /customers/15
Trong ví dụ trên, customers là tài nguyên, còn GET, POST và PATCH quyết định thao tác tương ứng. Cấu trúc này giúp endpoint ngắn hơn và giảm số lượng URL cần quản lý.
Đối với tài nguyên dạng tập hợp, nên dùng tên số nhiều nếu đó là quy ước chung của dự án. Chẳng hạn, sử dụng /users và /orders sẽ dễ thống nhất hơn nếu toàn bộ API đều theo cùng một cách đặt tên.
Tài nguyên con cần thể hiện đúng quan hệ
Khi một tài nguyên phụ thuộc trực tiếp vào tài nguyên cha, URL có thể thể hiện mối quan hệ đó.
GET /orders/125/items
GET /products/20/reviews
Cách này phù hợp khi ngữ cảnh của tài nguyên con thực sự phụ thuộc vào tài nguyên cha. Tuy nhiên, không nên tạo URL quá sâu chỉ để mô tả toàn bộ quan hệ trong cơ sở dữ liệu.
Một cấu trúc như /customers/15/orders/125/items/8 có thể trở nên khó sử dụng nếu được áp dụng tràn lan. API nên phản ánh mối quan hệ có ý nghĩa đối với client thay vì sao chép nguyên trạng cấu trúc bảng trong database.
Cấu trúc URL cần nhất quán ngay từ API đầu tiên
URL là một trong những phần mà người sử dụng API nhìn thấy nhiều nhất. Vì vậy, cách đặt URL cần được quy định từ đầu thay vì mỗi lập trình viên tự lựa chọn một kiểu.
Một cấu trúc cơ bản có thể được tổ chức như sau:
/api/v1/products
/api/v1/products/25
/api/v1/categories
/api/v1/orders/125
Trong đó, phần /api giúp phân biệt khu vực giao tiếp lập trình với các tài nguyên khác của website, còn /v1 có thể được sử dụng khi hệ thống cần quản lý phiên bản API.
Điều quan trọng không phải là phải dùng chính xác chuỗi /api/v1, mà là phải có một quy ước rõ ràng và áp dụng thống nhất. Một dự án có thể lựa chọn cấu trúc khác nếu phù hợp với kiến trúc tổng thể.
Không nên đưa thông tin kỹ thuật nội bộ vào URL
URL của API nên mô tả tài nguyên ở góc nhìn của client thay vì để lộ cách hệ thống lưu trữ dữ liệu.
Ví dụ, nếu hệ thống sử dụng bảng tbl_product_data trong database thì không có nghĩa endpoint phải mang tên đó.
GET /tbl_product_data
GET /tbl_product_data/25
Đây là cách thiết kế không tốt vì tên bảng là chi tiết triển khai bên trong. Nếu sau này database được tổ chức lại, API có thể bị ảnh hưởng không cần thiết.
Thay vào đó, nên giữ API độc lập với cấu trúc lưu trữ:
GET /products
GET /products/25
Client chỉ cần quan tâm rằng hệ thống cung cấp tài nguyên sản phẩm. Việc dữ liệu được lưu trong MySQL, PostgreSQL hay một hệ thống khác không nên trở thành một phần của hợp đồng API.
Query parameter dành cho lọc và điều khiển tập kết quả
Query parameter phù hợp với những trường hợp client muốn thay đổi cách lấy một tập tài nguyên mà không làm thay đổi bản chất endpoint.
GET /products?category=phone
GET /products?status=active
GET /products?keyword=iphone
Khi có nhiều điều kiện, các tham số có thể kết hợp với nhau:
GET /products?category=phone&status=active
Điểm cần chú ý là query parameter nên phục vụ việc lọc, tìm kiếm, sắp xếp, phân trang hoặc điều khiển cách trả về dữ liệu. Không nên biến query string thành một nơi chứa toàn bộ logic nghiệp vụ của hệ thống.
HTTP method phải phản ánh đúng ý nghĩa của thao tác
Một REST API rõ ràng cần sử dụng HTTP method theo đúng mục đích. Đây là phần rất quan trọng vì client có thể dựa vào method để hiểu bản chất của request mà không cần đọc sâu vào tài liệu API.
| Method | Mục đích chính | Ví dụ |
|---|---|---|
| GET | Lấy dữ liệu | /products/25 |
| POST | Tạo tài nguyên mới hoặc thực hiện thao tác tạo | /products |
| PUT | Cập nhật hoặc thay thế toàn bộ biểu diễn tài nguyên | /products/25 |
| PATCH | Cập nhật một phần tài nguyên | /products/25 |
| DELETE | Xóa tài nguyên | /products/25 |
Việc sử dụng method đúng còn giúp API trở nên dễ đoán hơn. Khi client gửi GET, người dùng API kỳ vọng dữ liệu được đọc chứ không phải một thao tác làm thay đổi trạng thái hệ thống.
GET không nên tạo ra thay đổi dữ liệu
Một nguyên tắc quan trọng là request GET nên được sử dụng cho việc đọc dữ liệu. Không nên lợi dụng GET để thực hiện các thao tác cập nhật hoặc xóa.
Cách thiết kế như sau nên tránh:
GET /delete-product?id=25
GET /update-product?id=25&status=active
Việc này làm sai ý nghĩa của HTTP method và có thể gây ra nhiều vấn đề khi trình duyệt, crawler, cache hoặc các hệ thống trung gian xử lý request GET theo cách không phù hợp với một thao tác thay đổi dữ liệu.
Thay vào đó, endpoint nên sử dụng method tương ứng:
DELETE /products/25
PATCH /products/25
PUT và PATCH không nên bị sử dụng tùy tiện
PUT thường được hiểu theo hướng gửi một biểu diễn đầy đủ để thay thế hoặc cập nhật tài nguyên theo quy ước của API. PATCH phù hợp hơn khi client chỉ muốn thay đổi một phần dữ liệu.
Ví dụ, sản phẩm hiện có nhiều thuộc tính nhưng client chỉ cần thay đổi trạng thái:
PATCH /products/25
Dữ liệu gửi lên có thể chỉ chứa trường cần thay đổi:
{
"status": "active"
}
Điều quan trọng là server phải xác định rõ hợp đồng của từng endpoint. Nếu một endpoint dùng PUT nhưng lại xử lý giống PATCH, hoặc ngược lại, client rất khó dự đoán dữ liệu nào sẽ được giữ lại và dữ liệu nào bị thay thế.
Thiết kế endpoint theo nghiệp vụ thay vì sao chép database
Một sai lầm phổ biến khi xây dựng REST API là lấy tên bảng database làm endpoint rồi mở trực tiếp các thao tác CRUD. Cách này có thể giúp tạo API nhanh trong giai đoạn đầu nhưng thường gây khó khăn khi nghiệp vụ phát triển.
Database và API có hai mục đích khác nhau. Database tối ưu cho việc lưu trữ và truy vấn dữ liệu, trong khi API là hợp đồng giao tiếp giữa các thành phần của hệ thống.
Ví dụ, database có thể tách thông tin đơn hàng thành nhiều bảng để phục vụ hiệu năng. Client không nhất thiết phải biết những bảng đó tồn tại.
GET /orders/125
Response của endpoint có thể tổng hợp những dữ liệu cần thiết từ nhiều nguồn khác nhau. Client chỉ nhận được một mô hình dữ liệu có ý nghĩa đối với nghiệp vụ.
Điều này tạo ra một lớp trừu tượng quan trọng: database có thể thay đổi mà API không nhất thiết phải thay đổi theo. Đây là nền tảng giúp hệ thống dễ bảo trì khi kiến trúc bên trong ngày càng phức tạp.
Một endpoint nên có trách nhiệm rõ ràng
Endpoint không nên trở thành một điểm xử lý quá nhiều loại nghiệp vụ chỉ vì chúng liên quan đến cùng một bảng dữ liệu.
Ví dụ, việc lấy danh sách đơn hàng và việc xác nhận đơn hàng có thể liên quan đến cùng một tài nguyên nhưng không nhất thiết phải xử lý theo cùng một cách. Với nghiệp vụ đặc biệt, API có thể cần một endpoint hành động rõ ràng nếu việc đó thực sự mang tính nghiệp vụ và không thể diễn đạt hợp lý bằng CRUD đơn thuần.
POST /orders/125/cancel
POST /orders/125/confirm
Cách tiếp cận này không mâu thuẫn với tư duy REST nếu được sử dụng có kiểm soát. Mục tiêu không phải ép mọi nghiệp vụ vào một công thức cứng nhắc, mà là xây dựng một API có ngữ nghĩa rõ ràng, nhất quán và dễ sử dụng.
Một REST API tốt không phải là API có thật nhiều endpoint, mà là API khiến người sử dụng có thể dự đoán được cách giao tiếp với hệ thống.
Không chỉ URL và HTTP method, chất lượng của REST API còn phụ thuộc rất lớn vào cách dữ liệu được gửi đi và trả về. Nếu response thiếu nhất quán, lỗi không rõ nguyên nhân hoặc danh sách dữ liệu không có cơ chế phân trang, API sẽ nhanh chóng trở thành điểm gây khó khăn cho frontend và các hệ thống tích hợp.
Cấu trúc request và response cần có quy ước rõ ràng
Request là dữ liệu client gửi lên server, còn response là kết quả server trả về. Hai chiều giao tiếp này nên được quy định nhất quán ngay từ đầu để frontend, mobile app hoặc đối tác tích hợp không phải đoán cấu trúc dữ liệu.
Với API hiện đại, JSON thường là lựa chọn phù hợp cho dữ liệu nghiệp vụ vì dễ đọc, dễ xử lý và được hỗ trợ rộng rãi. Tuy nhiên, điều quan trọng hơn định dạng là cách tổ chức các trường bên trong phải ổn định.
Ví dụ một request tạo sản phẩm có thể được thiết kế như sau:
{
"name": "Điện thoại ABC",
"price": 12500000,
"status": "active"
}
Server nhận dữ liệu, kiểm tra tính hợp lệ rồi tạo tài nguyên. Response thành công nên cung cấp những thông tin mà client thực sự cần, chẳng hạn mã tài nguyên, dữ liệu hiện tại và trạng thái xử lý.
{
"data": {
"id": 125,
"name": "Điện thoại ABC",
"price": 12500000,
"status": "active"
}
}
Không nên trả về quá nhiều dữ liệu nội bộ chỉ vì server đang có sẵn chúng. Response tốt là response phù hợp với nhu cầu của client và không làm lộ những thông tin không nên xuất hiện bên ngoài.
Không nên để mỗi endpoint có một kiểu response riêng
Nếu một endpoint trả trực tiếp object, endpoint khác trả mảng, endpoint khác lại bọc dữ liệu trong nhiều lớp không có quy tắc, phía client sẽ phải viết nhiều cách xử lý khác nhau.
Ví dụ, API có thể thống nhất dữ liệu chính nằm trong trường data. Với danh sách, phần thông tin phân trang có thể được đặt trong meta.
{
"data": [
{
"id": 1,
"name": "Sản phẩm A"
},
{
"id": 2,
"name": "Sản phẩm B"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 125
}
}
Không nhất thiết mọi dự án phải sử dụng đúng tên data và meta. Điều quan trọng là quy ước được lựa chọn phải nhất quán trên toàn bộ API.
HTTP status code phải phản ánh đúng kết quả xử lý
Status code giúp client biết request đã được xử lý như thế nào mà không cần phân tích nội dung response trước. Vì vậy, không nên sử dụng một mã duy nhất cho mọi trường hợp.
| Status code | Ý nghĩa thường gặp | Trường hợp sử dụng |
|---|---|---|
| 200 | OK | Request thành công và server trả về kết quả |
| 201 | Created | Tạo thành công một tài nguyên mới |
| 204 | No Content | Xử lý thành công nhưng không cần trả nội dung |
| 400 | Bad Request | Request không hợp lệ về mặt cú pháp hoặc dữ liệu đầu vào |
| 401 | Unauthorized | Chưa xác thực hoặc thông tin xác thực không hợp lệ |
| 403 | Forbidden | Đã xác thực nhưng không có quyền thực hiện thao tác |
| 404 | Not Found | Tài nguyên hoặc endpoint được yêu cầu không tồn tại |
| 409 | Conflict | Request tạo ra xung đột với trạng thái hiện tại |
| 422 | Unprocessable Content | Dữ liệu có cấu trúc hợp lệ nhưng không đáp ứng quy tắc nghiệp vụ hoặc validation |
| 429 | Too Many Requests | Client gửi request vượt giới hạn cho phép |
| 500 | Internal Server Error | Lỗi ngoài dự kiến xảy ra phía server |
Ví dụ, khi tạo thành công một sản phẩm mới, sử dụng 201 có ý nghĩa hơn việc luôn trả về 200. Khi tài nguyên không tồn tại, 404 giúp client phân biệt rõ với một lỗi máy chủ.
Status code không thay thế cho nội dung response. Nó nên kết hợp với một cấu trúc lỗi nhất quán để client có đủ thông tin xử lý tình huống.
Thông báo lỗi phải giúp tìm đúng nguyên nhân
Một API chuyên nghiệp không nên trả về những thông báo chung chung như Có lỗi xảy ra trong mọi trường hợp. Thông báo như vậy có thể đủ để người dùng biết thao tác thất bại nhưng không đủ thông tin cho frontend hoặc hệ thống tích hợp xử lý chính xác.
Response lỗi nên cung cấp thông tin ở mức cần thiết, chẳng hạn mã lỗi nội bộ, thông báo tổng quát và danh sách trường bị lỗi.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Dữ liệu gửi lên chưa hợp lệ.",
"fields": {
"email": "Email không đúng định dạng.",
"price": "Giá phải lớn hơn 0."
}
}
}
Cách này đặc biệt hữu ích với các form trên website. Frontend có thể dựa vào tên trường để hiển thị lỗi đúng vị trí thay vì phải phân tích một đoạn văn bản dài.
Mã lỗi nghiệp vụ nên ổn định
Thông báo dành cho con người có thể thay đổi theo giao diện hoặc ngôn ngữ, nhưng mã lỗi dành cho máy nên ổn định.
{
"error": {
"code": "ORDER_ALREADY_CANCELLED",
"message": "Đơn hàng đã được hủy trước đó."
}
}
Frontend có thể xử lý ORDER_ALREADY_CANCELLED theo một logic cụ thể mà không phụ thuộc vào việc nội dung message được viết bằng tiếng Việt, tiếng Anh hay được thay đổi trong tương lai.
Không trả lỗi nội bộ cho client
Thông tin như câu lệnh SQL, đường dẫn file trên server, stack trace, khóa bí mật hoặc cấu trúc hệ thống nội bộ không nên xuất hiện trong response gửi ra ngoài.
Trong môi trường phát triển, log chi tiết có thể được lưu phía server để lập trình viên điều tra. API bên ngoài chỉ nên trả thông tin đủ để client hiểu và xử lý lỗi.
Validation phải được thực hiện ở ranh giới API
Dữ liệu từ client luôn cần được xem là dữ liệu chưa đáng tin cậy. Server phải tự kiểm tra trước khi đưa dữ liệu vào tầng nghiệp vụ hoặc database.
Validation có thể bao gồm kiểu dữ liệu, độ dài, giá trị cho phép, định dạng, trường bắt buộc và mối quan hệ giữa nhiều trường.
{
"name": "",
"price": -500,
"status": "unknown"
}
Request trên có thể vi phạm nhiều quy tắc cùng lúc. API nên xác định và trả về lỗi đủ rõ để client biết từng trường cần điều chỉnh như thế nào.
Validation ở server vẫn cần thiết ngay cả khi frontend đã kiểm tra dữ liệu. Kiểm tra phía client chủ yếu giúp cải thiện trải nghiệm, còn server mới là nơi quyết định dữ liệu có được chấp nhận hay không.
Validation không chỉ là kiểm tra kiểu dữ liệu
Một giá trị có thể đúng kiểu nhưng vẫn sai nghiệp vụ. Ví dụ, price là số nhưng không được phép âm; status là chuỗi nhưng chỉ có một số giá trị được chấp nhận.
Đối với những quy tắc liên quan đến trạng thái hệ thống, API còn cần kiểm tra dữ liệu hiện tại trước khi thực hiện thay đổi.
Chẳng hạn, một đơn hàng đã hoàn tất có thể không được chuyển ngược về trạng thái đang xử lý. Đây không còn là validation đơn thuần của dữ liệu đầu vào mà là kiểm tra tính hợp lệ của thao tác trong bối cảnh nghiệp vụ.
Phân trang là yêu cầu quan trọng với API trả danh sách
Không nên mặc định trả toàn bộ dữ liệu khi endpoint có thể chứa hàng nghìn hoặc hàng triệu bản ghi. Một request lấy danh sách lớn có thể làm tăng thời gian truy vấn, dung lượng response, bộ nhớ và băng thông.
Phân trang giúp giới hạn số bản ghi mỗi lần trả về.
GET /products?page=1&per_page=20
Response có thể cung cấp thông tin để client biết đang đứng ở trang nào và còn bao nhiêu dữ liệu.
{
"data": [
{
"id": 1,
"name": "Sản phẩm A"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 125,
"total_pages": 7
}
}
Giới hạn mặc định và giới hạn tối đa của per_page cũng nên được xác định. Nếu client có thể gửi một giá trị quá lớn, endpoint vẫn có nguy cơ phải xử lý một lượng dữ liệu không phù hợp.
Lựa chọn kiểu phân trang theo quy mô dữ liệu
Phân trang theo số trang phù hợp với nhiều màn hình quản trị và danh sách có quy mô vừa phải. Tuy nhiên, khi dữ liệu rất lớn hoặc thường xuyên thay đổi, cách phân trang dựa hoàn toàn vào số thứ tự trang có thể trở nên kém hiệu quả.
Với những hệ thống có lượng dữ liệu lớn, cursor-based pagination có thể là lựa chọn đáng cân nhắc. Client nhận một cursor đại diện cho vị trí tiếp theo thay vì yêu cầu server tính toán lại toàn bộ vị trí của trang.
GET /products?limit=20&cursor=eyJpZCI6MTI1fQ
Không có một phương pháp phân trang duy nhất phù hợp cho mọi hệ thống. Quyết định nên dựa trên kích thước dữ liệu, tần suất thay đổi, yêu cầu giao diện và khả năng của hệ quản trị cơ sở dữ liệu.
Lọc và sắp xếp cần có giới hạn hợp lý
API danh sách thường cần hỗ trợ lọc theo trạng thái, danh mục, khoảng giá, thời gian hoặc từ khóa. Query parameter là cách phù hợp để truyền các điều kiện này.
GET /products?status=active&category=phone
GET /orders?status=completed&created_from=2026-01-01
GET /products?keyword=iphone
Đối với sắp xếp, API có thể định nghĩa một danh sách trường được phép sử dụng:
GET /products?sort=price
GET /products?sort=-created_at
Trong ví dụ trên, dấu - có thể được quy ước là sắp xếp giảm dần. Đây chỉ là một quy ước minh họa; dự án có thể lựa chọn cách biểu diễn khác miễn là tài liệu và implementation thống nhất.
Đặc biệt, không nên cho client truyền trực tiếp biểu thức SQL vào tham số sort hoặc filter. Server phải kiểm soát danh sách trường được phép và xây dựng truy vấn bằng cơ chế an toàn.
API càng cho phép client điều khiển nhiều thứ, server càng cần kiểm soát chặt những giá trị được phép nhận. Linh hoạt không đồng nghĩa với việc mở toàn bộ quyền truy vấn cho bên ngoài.
Xác thực và phân quyền phải được tách thành hai lớp rõ ràng
Trong một REST API thực tế, việc xác định người gửi request là ai và người đó được phép làm gì là hai vấn đề khác nhau. Authentication trả lời câu hỏi danh tính, còn Authorization quyết định phạm vi hành động mà danh tính đó được phép thực hiện.
Ví dụ, một người dùng có thể đăng nhập thành công nhưng không đồng nghĩa với việc họ được phép xem dữ liệu quản trị hoặc xóa đơn hàng của người khác.
API cần kiểm tra quyền ở phía server đối với từng tài nguyên và thao tác quan trọng. Không nên chỉ ẩn nút trên giao diện rồi xem đó là cơ chế phân quyền, bởi client hoàn toàn có thể tự tạo request mà không thông qua giao diện.
Token phải được xử lý như thông tin nhạy cảm
Nếu API sử dụng token để xác thực, token phải được truyền qua kết nối HTTPS và được bảo vệ trong toàn bộ vòng đời. Không nên ghi token vào log, đưa token vào URL hoặc trả token trong những nơi không cần thiết.
Ví dụ request có thể sử dụng header Authorization:
Authorization: Bearer ACCESS_TOKEN
Server cần kiểm tra token trước khi cho phép truy cập tài nguyên yêu cầu xác thực. Khi token hết hạn hoặc không hợp lệ, response phải thể hiện đúng trạng thái để client có thể xử lý.
Với hệ thống có nhiều loại người dùng, việc phân quyền cũng nên được thiết kế theo nghiệp vụ thay vì chỉ dựa vào một biến is_admin. Khi hệ thống phát triển, có thể cần nhiều vai trò và quyền hạn khác nhau.
{
"role": "editor",
"permissions": [
"product.read",
"product.update"
]
}
Cách tổ chức quyền thành các khả năng cụ thể giúp hệ thống linh hoạt hơn khi số lượng vai trò tăng lên.
Bảo mật API phải được tính từ thiết kế, không phải bổ sung sau cùng
REST API thường nằm ở ranh giới giữa Internet và hệ thống phía server nên là một trong những khu vực cần được kiểm soát chặt chẽ. Bảo mật không chỉ là xác thực người dùng mà bao gồm cả kiểm soát dữ liệu đầu vào, giới hạn request, bảo vệ thông tin nhạy cảm và hạn chế khả năng khai thác các tài nguyên bất hợp lý.
Trước hết, API công khai nên được triển khai qua HTTPS. Dữ liệu xác thực, thông tin cá nhân và nội dung nghiệp vụ không nên truyền qua kết nối không được mã hóa.
Server cũng phải kiểm tra quyền truy cập đến từng tài nguyên. Không được giả định rằng người dùng có quyền với một bản ghi chỉ vì họ biết được ID của bản ghi đó.
GET /orders/125
Việc biết mã đơn hàng 125 không đồng nghĩa với việc tài khoản đang đăng nhập được phép xem đơn hàng này. Server phải kiểm tra quan hệ giữa người dùng và tài nguyên trước khi trả dữ liệu.
Giới hạn tốc độ request
Rate limiting giúp hạn chế việc một client gửi quá nhiều request trong một khoảng thời gian. Đây là lớp bảo vệ hữu ích trước các hành vi lạm dụng, đồng thời giúp hệ thống tránh bị tiêu thụ tài nguyên quá mức bởi một nguồn request bất thường.
Ví dụ, API có thể giới hạn một nhóm endpoint công khai theo số request trong một khoảng thời gian nhất định. Khi vượt giới hạn, server có thể trả về 429 Too Many Requests và cung cấp thông tin phù hợp để client biết cần chờ trước khi thử lại.
Không nên đặt một mức giới hạn giống nhau cho tất cả endpoint. API đọc dữ liệu công khai, API đăng nhập và API thực hiện tác vụ nặng có thể cần những chính sách khác nhau.
Không để dữ liệu nhạy cảm lọt vào response
Response nên được xây dựng từ những trường dữ liệu thực sự cần thiết. Các thông tin như mật khẩu đã băm, token nội bộ, khóa bí mật hoặc dữ liệu phục vụ riêng cho backend không nên được trả về chỉ vì chúng tồn tại trong object.
Một nguyên tắc đơn giản là chỉ trả những gì client cần biết. Điều này vừa giảm kích thước response vừa thu hẹp lượng thông tin có thể bị khai thác nếu dữ liệu bị truy cập trái phép.
Versioning giúp API thay đổi mà không phá vỡ client cũ
API có thể tồn tại trong nhiều năm và được sử dụng bởi nhiều client khác nhau. Khi cấu trúc response hoặc cách hoạt động thay đổi, việc cập nhật server mà không quan tâm đến client cũ có thể gây ra lỗi hàng loạt.
Versioning giúp quản lý những thay đổi có khả năng phá vỡ tương thích.
GET /api/v1/products
GET /api/v2/products
Không phải mọi thay đổi đều cần tạo phiên bản mới. Việc bổ sung một trường response thường ít ảnh hưởng hơn việc đổi tên trường, xóa trường hoặc thay đổi ý nghĩa của dữ liệu hiện có.
Do đó, trước khi tạo phiên bản mới, cần xác định thay đổi có thực sự phá vỡ hợp đồng hiện tại hay không.
Không nên versioning tùy tiện
Nếu mỗi thay đổi nhỏ đều tạo một version mới, hệ thống có thể nhanh chóng phải duy trì quá nhiều phiên bản cùng lúc. Điều này làm tăng chi phí kiểm thử, tài liệu hóa và bảo trì.
Khi cần loại bỏ một phiên bản cũ, nên có kế hoạch chuyển đổi rõ ràng. Client cần biết phiên bản nào đang được sử dụng, thời điểm ngừng hỗ trợ và phương án chuyển sang phiên bản mới.
Versioning hiệu quả không chỉ nằm ở chuỗi v1 hay v2 trong URL mà nằm ở việc quản lý vòng đời của hợp đồng API một cách có chủ đích.
Idempotency giúp những thao tác quan trọng an toàn hơn
Trong môi trường mạng, request có thể bị timeout dù server đã xử lý thành công. Client khi đó có thể gửi lại request vì cho rằng lần đầu chưa thành công. Nếu thao tác tạo dữ liệu không được thiết kế phù hợp, một request có thể vô tình tạo ra nhiều bản ghi giống nhau.
Vấn đề này đặc biệt đáng chú ý với những nghiệp vụ như tạo đơn hàng, thanh toán hoặc khởi tạo một giao dịch.
Một cơ chế idempotency key có thể giúp server nhận biết những lần gửi lại cùng một thao tác.
POST /orders
Idempotency-Key: 7f1c8c0a-8d41-4f13-a8c4-125abc987654
Server có thể lưu trạng thái xử lý của key trong một khoảng thời gian phù hợp. Nếu client gửi lại request với cùng key, server có thể trả lại kết quả của lần xử lý trước thay vì thực hiện giao dịch lần nữa.
Idempotency không cần áp dụng máy móc cho mọi endpoint. Nó đặc biệt hữu ích với những thao tác có hậu quả nghiệp vụ quan trọng và có khả năng được client retry.
Thiết kế API cần tính đến hiệu năng ngay khi dữ liệu còn nhỏ
Một API có thể phản hồi rất nhanh khi database chỉ có vài nghìn bản ghi nhưng trở nên chậm khi dữ liệu tăng lên đáng kể. Vì vậy, hiệu năng nên được xem xét từ cách truy vấn dữ liệu, kích thước response cho tới số lượng request cần thiết để hoàn thành một màn hình.
Endpoint danh sách nên có giới hạn số lượng bản ghi trả về. Những trường không cần thiết cũng không nên đưa vào response chỉ vì chúng đang tồn tại trong database.
Nếu một màn hình cần thông tin tóm tắt, API có thể trả dữ liệu được thiết kế riêng cho nhu cầu đó thay vì tải toàn bộ thông tin chi tiết của từng đối tượng.
Cache có thể giảm tải cho những dữ liệu ít thay đổi
Những tài nguyên được truy cập thường xuyên nhưng ít thay đổi có thể hưởng lợi từ cơ chế cache. HTTP cung cấp nhiều cơ chế để client và các tầng trung gian biết khi nào một response có thể được sử dụng lại.
Tuy nhiên, cache cần được thiết kế dựa trên tính chất dữ liệu. Dữ liệu công khai và ít thay đổi có thể có chính sách cache khác hoàn toàn với thông tin cá nhân hoặc dữ liệu giao dịch.
Không nên bật cache một cách máy móc cho mọi endpoint. Nếu cache không được kiểm soát, client có thể nhận dữ liệu cũ hoặc nghiêm trọng hơn là nhận dữ liệu thuộc về một người dùng khác.
Tránh thiết kế API khiến client phải gọi quá nhiều lần
Một giao diện hiển thị danh sách sản phẩm có thể cần thêm thông tin danh mục, thương hiệu và đánh giá. Nếu mỗi dòng dữ liệu lại buộc frontend thực hiện thêm nhiều request riêng biệt, tổng số request có thể tăng rất nhanh.
API nên cân bằng giữa hai thái cực: response quá lớn và quá nhiều request nhỏ. Trong những trường hợp phù hợp, server có thể cung cấp dữ liệu liên quan trong cùng response hoặc hỗ trợ cơ chế lấy những trường cần thiết.
Mục tiêu không phải giảm số request bằng mọi giá mà là tạo ra luồng giao tiếp hợp lý giữa client và server.
Logging và monitoring giúp tìm lỗi khi API đi vào thực tế
API chạy ổn định trong môi trường phát triển chưa có nghĩa là hệ thống sẽ luôn hoạt động hoàn hảo khi có người dùng thực tế. Khi xảy ra lỗi, log và monitoring là cơ sở để xác định request nào gặp vấn đề, thời điểm xảy ra và tầng xử lý nào gây ra lỗi.
Mỗi request quan trọng nên có một mã nhận diện để có thể liên kết log giữa nhiều thành phần của hệ thống.
X-Request-ID: 8f4b2c91a72d4e31
Khi client nhận lỗi, mã này có thể được dùng để tra cứu log phía server mà không cần ghi toàn bộ dữ liệu nhạy cảm vào response.
Log nên tập trung vào thông tin phục vụ chẩn đoán như endpoint, method, thời gian xử lý, status code và mã request. Token, mật khẩu và dữ liệu nhạy cảm không nên được ghi nguyên trạng.
Theo dõi thời gian phản hồi và tỷ lệ lỗi
Hai API cùng trả về status 200 nhưng chất lượng có thể hoàn toàn khác nhau nếu một API phản hồi trong vài chục mili giây còn API kia thường xuyên mất vài giây.
Vì vậy, monitoring nên theo dõi ít nhất những chỉ số quan trọng như thời gian phản hồi, tỷ lệ lỗi, số lượng request và các endpoint có mức tiêu thụ tài nguyên bất thường.
Khi có dữ liệu theo thời gian, đội phát triển có thể phát hiện vấn đề trước khi người dùng phản ánh, đồng thời đánh giá được tác động của những thay đổi trong code hoặc database.
Tài liệu hóa là một phần của chất lượng REST API
Một API tốt nhưng không có tài liệu rõ ràng vẫn gây khó khăn cho người tích hợp. Tài liệu cần giúp người đọc biết endpoint nào tồn tại, request cần gửi gì, response nhận được gì, trạng thái lỗi ra sao và yêu cầu xác thực như thế nào.
Mỗi endpoint quan trọng nên có tối thiểu các thông tin:
- HTTP method và đường dẫn.
- Mục đích của endpoint.
- Header bắt buộc.
- Query parameter hoặc path parameter.
- Cấu trúc request body nếu có.
- Cấu trúc response thành công.
- Các status code có thể xuất hiện.
- Cấu trúc response lỗi.
- Quyền truy cập cần thiết.
Tài liệu cũng nên có ví dụ thực tế. Một ví dụ request và response ngắn, chính xác thường có giá trị hơn một đoạn mô tả dài nhưng không thể dùng để kiểm tra.
Checklist kiểm tra trước khi đưa API vào sử dụng
Trước khi hoàn thành một REST API, có thể kiểm tra theo một danh sách ngắn nhưng đủ thực tế thay vì chỉ kiểm tra xem endpoint có trả dữ liệu hay không.
- Endpoint đã đặt tên nhất quán và tập trung vào tài nguyên chưa?
- HTTP method có phản ánh đúng bản chất thao tác không?
- API có phân biệt rõ tài nguyên đơn và tập tài nguyên không?
- Query parameter có được giới hạn và kiểm soát giá trị đầu vào không?
- Response thành công có cấu trúc thống nhất không?
- Status code có phản ánh chính xác kết quả xử lý không?
- Response lỗi có mã lỗi ổn định và thông tin đủ để client xử lý không?
- Dữ liệu đầu vào đã được validation ở phía server chưa?
- API có kiểm tra quyền trên từng tài nguyên cần bảo vệ không?
- Thông tin nhạy cảm có bị trả về hoặc ghi vào log không?
- Các endpoint danh sách đã có giới hạn và phân trang phù hợp chưa?
- API có cơ chế hạn chế request bất thường không?
- Những nghiệp vụ quan trọng có cần idempotency hay không?
- Thay đổi có khả năng phá vỡ client cũ đã được quản lý version chưa?
- Thời gian phản hồi, tỷ lệ lỗi và hoạt động của API có được theo dõi không?
- Tài liệu API có đủ để một lập trình viên khác tích hợp mà không phải đoán không?
REST API được thiết kế tốt không nằm ở việc tuân theo thật nhiều quy tắc mà ở khả năng tạo ra một hợp đồng giao tiếp rõ ràng giữa các hệ thống. URL có ngữ nghĩa, method được dùng đúng mục đích, response nhất quán, lỗi dễ xử lý và quyền truy cập được kiểm soát sẽ tạo nên nền tảng vững chắc cho ứng dụng phía sau.
Với Web Mới, khi xây dựng API cho website hoặc hệ thống theo yêu cầu, những nguyên tắc này nên được xem là một phần của kiến trúc ngay từ đầu. Khi quy mô dữ liệu, số lượng người dùng và các hệ thống tích hợp tăng lên, một cấu trúc API được chuẩn bị tốt sẽ giúp giảm đáng kể chi phí sửa đổi và mở rộng về sau.
Quan trọng nhất, đừng thiết kế API chỉ để đáp ứng một màn hình hoặc một chức năng tại thời điểm hiện tại. Hãy coi API là một hợp đồng lâu dài giữa client và server: đủ đơn giản để sử dụng, đủ chặt chẽ để kiểm soát và đủ linh hoạt để hệ thống có thể phát triển.
- 0 Bình luận
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *