REST API là gì? Hướng dẫn RESTful API từ cơ bản đến nâng cao
Bùi Tấn Lực
- 106
- 05/09/2026
Trong quá trình xây dựng website và ứng dụng hiện đại, API là lớp kết nối giúp các hệ thống trao đổi dữ liệu với nhau. Một website có thể cần giao tiếp với ứng dụng di động, hệ thống quản trị, phần mềm bán hàng, dịch vụ thanh toán hoặc một nền tảng bên thứ ba. Nếu lớp giao tiếp này được thiết kế thiếu quy củ, hệ thống sẽ nhanh chóng trở nên khó mở rộng, khó bảo trì và khó tích hợp.
REST API là một trong những cách tiếp cận phổ biến nhất để giải quyết bài toán đó. Điểm quan trọng là REST không đơn giản chỉ là việc tạo một URL rồi trả về JSON. Một API được xem là RESTful tốt còn liên quan đến cách mô hình hóa tài nguyên, sử dụng HTTP method, mã trạng thái, cấu trúc request và response, quản lý phiên bản, xác thực, xử lý lỗi và nhiều nguyên tắc khác.
Trong bài viết này, Web Mới sẽ đi từ nền tảng của REST đến cách tư duy khi thiết kế một API thực tế. Mục tiêu không phải học thuộc một danh sách quy tắc, mà hiểu vì sao REST được thiết kế như vậy để có thể áp dụng đúng khi xây dựng hệ thống bằng PHP hoặc bất kỳ công nghệ backend nào.

REST API thực chất là gì và giải quyết bài toán nào?
REST là viết tắt của Representational State Transfer, một phong cách kiến trúc được Roy Fielding trình bày trong luận án tiến sĩ của ông. REST đưa ra một tập hợp các ràng buộc kiến trúc nhằm xây dựng những hệ thống phân tán có khả năng mở rộng và giao tiếp thông qua các giao thức chuẩn.
REST API có thể hiểu đơn giản là một API được thiết kế theo các nguyên tắc của REST, trong đó dữ liệu của hệ thống được tổ chức thành các tài nguyên và client tương tác với những tài nguyên đó thông qua HTTP.
Ví dụ, một website thương mại điện tử có thể có các tài nguyên:
- Sản phẩm
- Danh mục
- Khách hàng
- Đơn hàng
- Đánh giá
Thay vì thiết kế API theo hướng mỗi URL là một hành động như them-san-pham, sua-san-pham hoặc xoa-san-pham, REST thường mô hình hóa sản phẩm thành một tài nguyên và sử dụng HTTP method để biểu thị thao tác cần thực hiện.
GET /api/products
GET /api/products/125
POST /api/products
PUT /api/products/125
PATCH /api/products/125
DELETE /api/products/125
Cách tiếp cận này tạo ra một ngôn ngữ giao tiếp nhất quán giữa client và server. Người phát triển không cần đoán xem endpoint nào dùng để lấy dữ liệu, endpoint nào dùng để cập nhật hoặc endpoint nào dùng để xóa.
Đây chính là giá trị lớn của REST: tách cách client yêu cầu một tài nguyên khỏi cách server triển khai dữ liệu phía bên trong. Client không nhất thiết phải biết dữ liệu được lưu trong MySQL, PostgreSQL hay một hệ thống khác. Nó chỉ cần biết cách giao tiếp thông qua hợp đồng API.
REST API khác gì với một API thông thường?
API là khái niệm rộng, còn REST là một phong cách kiến trúc để thiết kế API. Vì vậy, không phải API nào cũng là REST API.
Một API có thể được xây dựng bằng nhiều cách khác nhau. Chẳng hạn, một hệ thống có thể sử dụng SOAP, GraphQL, RPC hoặc một giao thức tùy chỉnh. REST chỉ là một trong những cách tổ chức lớp giao tiếp đó.
Ví dụ một API thiên về RPC có thể được thiết kế như sau:
POST /api/createProduct
POST /api/updateProduct
POST /api/deleteProduct
POST /api/getProduct
Cách thiết kế này không nhất thiết sai. Tuy nhiên, nó đặt trọng tâm vào hành động. Trong REST, trọng tâm thường chuyển sang tài nguyên:
GET /api/products/125
POST /api/products
PATCH /api/products/125
DELETE /api/products/125
Như vậy, REST không phải một framework, cũng không phải một ngôn ngữ lập trình. Bạn có thể xây dựng REST API bằng PHP, Java, Python, Node.js, Go hoặc nhiều nền tảng khác.
Hiểu đúng tài nguyên, endpoint và representation
Muốn thiết kế REST API tốt, cần hiểu ba khái niệm thường xuyên xuất hiện cùng nhau: resource, endpoint và representation.
Resource là đối tượng mà API cung cấp khả năng truy cập. Với website bán hàng, sản phẩm có thể là một resource. Với hệ thống tuyển dụng, tin tuyển dụng, ứng viên và doanh nghiệp đều có thể trở thành resource.
Endpoint là địa chỉ mà client sử dụng để giao tiếp với resource. Ví dụ:
/api/products
/api/products/125
/api/orders
/api/orders/9001
Representation là cách tài nguyên được biểu diễn khi truyền qua mạng. Một sản phẩm trong cơ sở dữ liệu có thể được biểu diễn dưới dạng JSON khi trả về cho client.
{
"id": 125,
"name": "Máy lọc nước",
"price": 6500000,
"status": "active"
}
Điều này rất quan trọng vì resource và representation không phải là một. Resource nằm ở phía hệ thống, còn representation là dữ liệu được gửi qua giao tiếp API. Cùng một resource có thể được biểu diễn theo những cách khác nhau tùy thiết kế hệ thống.
Vì sao endpoint nên đại diện cho danh từ?
Một lỗi khá phổ biến khi mới xây dựng API là đưa động từ vào mọi URL. Ví dụ:
/api/getProducts
/api/getProduct
/api/createProduct
/api/updateProduct
/api/deleteProduct
Cách này khiến URL phải mô tả cả hành động, trong khi HTTP method vốn đã có nhiệm vụ biểu thị hành động đó.
Thiết kế gọn hơn là:
GET /api/products
GET /api/products/125
POST /api/products
PATCH /api/products/125
DELETE /api/products/125
Ở đây, products là tài nguyên, còn GET, POST, PATCH và DELETE cho biết client muốn thực hiện thao tác gì.
Không nên hiểu nguyên tắc này theo kiểu máy móc rằng mọi endpoint tuyệt đối không được chứa động từ. Trong hệ thống thực tế vẫn có những thao tác đặc biệt không phù hợp với mô hình CRUD thuần túy. Điều quan trọng là phần lớn API nên được mô hình hóa quanh tài nguyên thay vì biến URL thành danh sách các hàm backend.
RESTful API hoạt động như thế nào giữa client và server?
Một request REST thường bắt đầu từ client. Client có thể là trình duyệt, ứng dụng Android, iOS, frontend JavaScript hoặc một hệ thống khác. Request được gửi tới server thông qua HTTP hoặc HTTPS.
Server tiếp nhận request, xác thực quyền truy cập nếu cần, phân tích tham số, thực hiện nghiệp vụ, truy vấn hoặc thay đổi dữ liệu rồi trả response cho client.
Một luồng đơn giản có thể hình dung như sau:
- Client xác định resource cần truy cập.
- Client gửi HTTP request tới endpoint.
- Server kiểm tra request và quyền truy cập.
- Server xử lý nghiệp vụ.
- Server truy xuất hoặc thay đổi dữ liệu.
- Server tạo HTTP response.
- Client đọc status code, header và dữ liệu trả về.
Ví dụ client muốn lấy thông tin sản phẩm có ID 125:
GET /api/products/125 HTTP/1.1
Host: example.com
Accept: application/json
Server có thể trả về:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 125,
"name": "Máy lọc nước",
"price": 6500000,
"status": "active"
}
Client không cần biết server đã thực hiện bao nhiêu câu SQL phía sau request. Đối với client, hợp đồng giao tiếp nằm ở request và response.
REST có bắt buộc phải trả JSON không?
JSON xuất hiện rất phổ biến trong REST API vì có cấu trúc dễ đọc, kích thước tương đối gọn và được hỗ trợ tốt trên hầu hết ngôn ngữ lập trình. Tuy nhiên, REST không đồng nghĩa với JSON.
REST là phong cách kiến trúc, còn JSON là một định dạng biểu diễn dữ liệu. Một hệ thống REST có thể sử dụng những representation khác tùy nhu cầu. Trong thực tế web hiện đại, JSON thường là lựa chọn mặc định vì phù hợp với frontend, mobile app và các dịch vụ backend.
Vì vậy, câu nói REST API luôn phải trả JSON là cách hiểu quá đơn giản. Điều quan trọng hơn là API phải có hợp đồng dữ liệu rõ ràng và nhất quán giữa client với server.
Những ràng buộc cốt lõi tạo nên kiến trúc REST
REST không chỉ là việc đặt tên URL đẹp hoặc sử dụng JSON. Đằng sau REST là một tập hợp các ràng buộc kiến trúc. Hiểu những ràng buộc này giúp phân biệt một API được thiết kế có chủ đích với một API chỉ vô tình sử dụng HTTP.
Client và server được tách biệt
Client và server đảm nhiệm những trách nhiệm khác nhau. Client chịu trách nhiệm về giao diện hoặc trải nghiệm sử dụng, trong khi server chịu trách nhiệm xử lý dữ liệu và nghiệp vụ.
Sự tách biệt này giúp một backend có thể phục vụ nhiều loại client. Cùng một API có thể được sử dụng bởi website, ứng dụng điện thoại hoặc một phần mềm nội bộ mà không cần viết lại toàn bộ logic dữ liệu.
Stateless và ý nghĩa của việc không lưu trạng thái phiên trên server
Stateless là một trong những đặc điểm quan trọng nhất của REST. Mỗi request phải chứa đủ thông tin cần thiết để server hiểu và xử lý request đó. Server không nên phụ thuộc vào trạng thái của một request trước đó để hiểu request hiện tại.
Ví dụ client gửi:
GET /api/orders
Authorization: Bearer TOKEN
Server có thể sử dụng token trong request để xác định danh tính và quyền của client. Request tiếp theo cũng phải cung cấp thông tin cần thiết tương ứng thay vì bắt server phải dựa vào một phiên xử lý không rõ ràng từ request trước.
Stateless mang lại lợi ích lớn cho khả năng mở rộng. Khi có nhiều server backend, request có thể được phân phối tới các máy chủ khác nhau mà không nhất thiết phải giữ người dùng cố định trên một máy chủ cụ thể.
Tuy nhiên, stateless không có nghĩa là hệ thống không được lưu dữ liệu. Database vẫn lưu người dùng, đơn hàng, sản phẩm và các trạng thái nghiệp vụ. Ý nghĩa ở đây là server không phụ thuộc vào trạng thái giao tiếp tạm thời của client giữa các request.
Cacheable và khả năng tận dụng bộ nhớ đệm
REST cho phép response được xác định là có thể cache hoặc không thể cache. Điều này đặc biệt hữu ích với những dữ liệu được đọc thường xuyên nhưng ít thay đổi.
Ví dụ danh sách danh mục sản phẩm có thể được cache trong một khoảng thời gian phù hợp. Khi đó, không phải request nào cũng cần truy vấn trực tiếp database.
Việc cache cần được thiết kế cẩn thận. Dữ liệu cá nhân, thông tin nhạy cảm hoặc dữ liệu thay đổi liên tục không nên tùy tiện cache chỉ vì muốn tăng tốc độ.
Uniform Interface tạo ra cách giao tiếp nhất quán
Uniform Interface có thể xem là phần quan trọng để REST giữ được tính nhất quán. Client cần giao tiếp với tài nguyên thông qua một cách thức thống nhất thay vì phải hiểu chi tiết cách server triển khai bên trong.
Trong API HTTP, tính thống nhất thể hiện qua việc sử dụng URL, HTTP method, status code, header và representation theo những quy ước rõ ràng.
Ví dụ, nếu GET được dùng để đọc resource thì những endpoint đọc dữ liệu nên tuân theo cùng một cách tổ chức. Nếu lỗi xảy ra, response cũng nên có cấu trúc nhất quán để frontend dễ xử lý.
Layered System giúp hệ thống dễ mở rộng
Client không nhất thiết phải biết request của mình được xử lý trực tiếp bởi application server hay đi qua nhiều lớp trung gian.
Giữa client và server có thể xuất hiện load balancer, reverse proxy, cache, gateway hoặc các lớp bảo mật. Nếu hệ thống được thiết kế đúng, client vẫn giao tiếp với API theo cùng một hợp đồng.
Đây là một trong những lý do REST phù hợp với các hệ thống có quy mô từ nhỏ đến lớn.
HTTP method trong REST không chỉ là GET và POST
HTTP method là công cụ quan trọng giúp REST biểu đạt ý định của client. Việc hiểu đúng từng method sẽ giúp endpoint rõ ràng hơn và tránh thiết kế API theo kiểu tất cả thao tác đều dùng POST.
| Method | Mục đích thường gặp | Ví dụ |
|---|---|---|
| GET | Lấy representation của resource | /api/products/125 |
| POST | Tạo resource hoặc thực hiện thao tác không phù hợp với việc cập nhật trực tiếp một resource | /api/products |
| PUT | Thay thế representation của resource | /api/products/125 |
| PATCH | Cập nhật một phần resource | /api/products/125 |
| DELETE | Xóa resource | /api/products/125 |
Điểm cần chú ý là PUT và PATCH không hoàn toàn giống nhau. PUT thường được hiểu theo hướng thay thế representation của resource, trong khi PATCH được dùng khi chỉ muốn thay đổi một phần.
Ví dụ sản phẩm có nhiều thuộc tính nhưng chỉ muốn thay đổi trạng thái:
PATCH /api/products/125
{
"status": "inactive"
}
Cách này tránh việc client phải gửi lại toàn bộ representation của sản phẩm chỉ để thay đổi một trường.
Idempotent là gì và vì sao cần quan tâm?
Idempotent mô tả đặc tính trong đó việc thực hiện cùng một request nhiều lần có hiệu ứng tương đương về mặt trạng thái của resource với việc thực hiện một lần.
GET thường được xem là idempotent vì đọc cùng một resource nhiều lần không nhằm tạo thêm dữ liệu mới. PUT và DELETE cũng có tính chất idempotent theo ngữ nghĩa HTTP, dù response cụ thể giữa các lần gọi có thể khác nhau.
POST thường không được xem là idempotent. Nếu client gửi cùng một request tạo đơn hàng hai lần, hệ thống có thể tạo hai đơn hàng.
Điều này đặc biệt quan trọng trong các hệ thống thanh toán, đặt hàng và giao dịch. Khi mạng chập chờn, client có thể không biết request trước đã được server xử lý hay chưa và gửi lại request. Nếu nghiệp vụ không có cơ chế chống xử lý trùng, một thao tác có thể bị thực hiện nhiều lần.
Do đó, ở những API quan trọng, cần xem xét thiết kế idempotency key hoặc một cơ chế tương đương để kiểm soát request lặp lại. Đây là vấn đề nghiệp vụ và độ tin cậy, không chỉ là vấn đề cú pháp HTTP.
Thiết kế endpoint REST API sao cho rõ ràng và dễ mở rộng
Một endpoint tốt không chỉ cần chạy đúng mà còn phải giúp người khác nhanh chóng hiểu API đang cung cấp tài nguyên nào và có thể làm gì với tài nguyên đó. Khi hệ thống phát triển từ vài endpoint lên hàng chục hoặc hàng trăm endpoint, cách đặt tên và tổ chức URL sẽ ảnh hưởng trực tiếp đến khả năng bảo trì.
Nguyên tắc dễ áp dụng nhất là bắt đầu từ tài nguyên thay vì bắt đầu từ hành động. Nếu hệ thống có tài nguyên sản phẩm, có thể sử dụng:
/api/products
/api/products/125
Endpoint đầu tiên đại diện cho tập hợp sản phẩm, còn endpoint thứ hai đại diện cho một sản phẩm cụ thể có ID 125.
Từ đó, HTTP method quyết định thao tác:
GET /api/products Lấy danh sách
POST /api/products Tạo sản phẩm
GET /api/products/125 Lấy sản phẩm 125
PUT /api/products/125 Thay thế sản phẩm 125
PATCH /api/products/125 Cập nhật một phần sản phẩm 125
DELETE /api/products/125 Xóa sản phẩm 125
Cách tổ chức này có ưu điểm lớn là URL không phải thay đổi chỉ vì thao tác thay đổi. Resource vẫn là products, còn method thể hiện ý định.
Danh sách và tài nguyên đơn lẻ nên được phân biệt
Một API thường cần hai cấp độ truy cập: collection và individual resource.
Ví dụ:
GET /api/products
GET /api/products/125
Request đầu tiên yêu cầu một tập hợp sản phẩm. Request thứ hai yêu cầu đúng một sản phẩm.
Không nên thiết kế danh sách bằng những URL khó đoán như:
GET /api/products-list
GET /api/all-products
GET /api/product-detail?id=125
Những cách này không phải lúc nào cũng khiến hệ thống sai về mặt kỹ thuật, nhưng chúng làm API thiếu tính nhất quán. Khi số lượng tài nguyên tăng lên, việc duy trì một quy ước URL thống nhất sẽ có giá trị hơn rất nhiều so với việc đặt tên tùy ý cho từng endpoint.
Tài nguyên con và quan hệ giữa các đối tượng
Trong hệ thống thực tế, tài nguyên thường có quan hệ với nhau. Một đơn hàng có thể chứa nhiều sản phẩm, một doanh nghiệp có nhiều tin tuyển dụng hoặc một bài viết có nhiều bình luận.
Ví dụ có thể biểu diễn các bình luận của bài viết như:
GET /api/posts/25/comments
Endpoint này thể hiện rõ rằng comments đang được truy cập trong phạm vi của bài viết có ID 25.
Tuy nhiên, không nên lồng URL quá sâu:
/api/companies/10/jobs/25/applicants/300/skills/5
URL quá sâu khiến API khó sử dụng và khó bảo trì. Khi quan hệ giữa các tài nguyên trở nên phức tạp, nên cân nhắc truy cập trực tiếp resource bằng ID hoặc sử dụng query parameter để biểu diễn điều kiện lọc.
CRUD trong REST API và cách chuyển nghiệp vụ thành HTTP
CRUD là viết tắt của Create, Read, Update và Delete. Đây là mô hình rất phổ biến trong các hệ thống quản lý dữ liệu và cũng là nền tảng dễ hiểu để bắt đầu thiết kế REST API.
| Nghiệp vụ | Method | Endpoint |
|---|---|---|
| Tạo sản phẩm | POST | /api/products |
| Lấy danh sách | GET | /api/products |
| Lấy một sản phẩm | GET | /api/products/125 |
| Cập nhật toàn bộ | PUT | /api/products/125 |
| Cập nhật một phần | PATCH | /api/products/125 |
| Xóa | DELETE | /api/products/125 |
Điểm quan trọng là CRUD chỉ là mô hình dữ liệu cơ bản. Một hệ thống thực tế thường có những nghiệp vụ phức tạp hơn như xác nhận đơn hàng, hoàn tiền, gửi lại email hoặc kích hoạt tài khoản.
Không nên cố ép mọi nghiệp vụ vào CRUD nếu điều đó khiến API trở nên khó hiểu. Hãy ưu tiên một API có ngữ nghĩa rõ ràng thay vì cố đạt được một hình thức REST cứng nhắc.
Tạo tài nguyên bằng POST
Khi client muốn tạo một sản phẩm mới, request thường gửi dữ liệu trong body:
POST /api/products
Content-Type: application/json
{
"name": "Máy lọc nước",
"price": 6500000,
"category_id": 8
}
Server nhận dữ liệu, kiểm tra tính hợp lệ, thực hiện nghiệp vụ và lưu vào database. Nếu tạo thành công, response nên cung cấp thông tin đủ để client biết resource vừa được tạo.
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 125,
"name": "Máy lọc nước",
"price": 6500000,
"category_id": 8
}
Trong trường hợp phù hợp, header Location cũng có thể được sử dụng để chỉ tới resource vừa tạo.
Cập nhật toàn bộ và cập nhật một phần
PUT và PATCH thường gây nhầm lẫn đối với người mới xây dựng API.
PUT thiên về việc gửi representation mới cho resource. Ví dụ:
PUT /api/products/125
Content-Type: application/json
{
"name": "Máy lọc nước cao cấp",
"price": 7200000,
"category_id": 8,
"status": "active"
}
Trong khi đó, PATCH phù hợp hơn khi chỉ thay đổi một phần:
PATCH /api/products/125
Content-Type: application/json
{
"price": 7200000
}
Điều quan trọng không phải chọn method theo thói quen mà phải thống nhất semantics trong toàn bộ API. Nếu đã quy định PATCH dùng cho cập nhật từng phần, các endpoint khác cũng nên tuân theo quy ước đó.
Xóa dữ liệu không nhất thiết đồng nghĩa với xóa vật lý
DELETE biểu thị ý định xóa resource khỏi phạm vi truy cập thông thường của API. Tuy nhiên, backend có thể thực hiện xóa vật lý hoặc soft delete tùy yêu cầu nghiệp vụ.
Ví dụ một sản phẩm bị ngừng kinh doanh có thể được đánh dấu:
{
"status": "deleted"
}
thay vì xóa hoàn toàn bản ghi khỏi database. Đây là quyết định của tầng nghiệp vụ và dữ liệu, không phải REST bắt buộc phải quyết định thay bạn.
HTTP request gồm những thành phần nào?
Muốn làm REST API chuyên nghiệp, không thể chỉ quan tâm đến URL. Một HTTP request thường bao gồm method, URL, header và có thể có body.
Ví dụ:
POST /api/products HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer TOKEN
{
"name": "Máy lọc nước",
"price": 6500000
}
Trong request này, POST là method, /api/products là đường dẫn, các dòng Host, Content-Type và Authorization là header, còn object JSON là body.
Path parameter và query parameter
Path parameter thường dùng để xác định resource cụ thể:
GET /api/products/125
Ở đây 125 là định danh của sản phẩm.
Query parameter thường phù hợp với việc lọc, tìm kiếm, sắp xếp hoặc phân trang:
GET /api/products?category_id=8&status=active
Hoặc:
GET /api/products?page=2&limit=20
Cách phân chia này giúp URL có ngữ nghĩa rõ ràng. ID xác định resource, còn query string điều khiển cách truy vấn collection.
Header có vai trò gì?
Header truyền các thông tin bổ sung về request hoặc response. Trong REST API, một số header xuất hiện thường xuyên gồm:
- Content-Type: cho biết định dạng dữ liệu được gửi.
- Accept: cho biết định dạng client mong muốn nhận.
- Authorization: mang thông tin xác thực theo cơ chế được hệ thống lựa chọn.
- Cache-Control: điều khiển một số chính sách cache.
Ví dụ:
Accept: application/json
Content-Type: application/json
Việc sử dụng header đúng mục đích giúp phần giao tiếp rõ ràng hơn thay vì nhét tất cả thông tin vào URL hoặc body.
Thiết kế response nhất quán để frontend dễ xử lý
API có thể trả dữ liệu đúng nhưng vẫn khó sử dụng nếu mỗi endpoint có một cấu trúc response khác nhau. Frontend khi đó phải viết rất nhiều logic riêng để xử lý từng trường hợp.
Một response thành công có thể được thiết kế theo cấu trúc:
{
"data": {
"id": 125,
"name": "Máy lọc nước",
"price": 6500000
}
}
Với danh sách:
{
"data": [
{
"id": 125,
"name": "Máy lọc nước",
"price": 6500000
},
{
"id": 126,
"name": "Máy lọc nước nóng lạnh",
"price": 8200000
}
]
}
Không có một cấu trúc JSON duy nhất bắt buộc cho mọi REST API. Điều quan trọng là dự án phải lựa chọn một quy ước và sử dụng ổn định.
Không nên trả quá nhiều dữ liệu không cần thiết
Một API lấy danh sách sản phẩm không nhất thiết phải trả toàn bộ thông tin mà database đang có. Nếu bảng sản phẩm chứa hàng chục trường nhưng giao diện chỉ cần ID, tên, giá và ảnh đại diện, response có thể chỉ trả những dữ liệu đó.
Việc giới hạn dữ liệu giúp giảm kích thước response, giảm thời gian truyền và tránh vô tình làm lộ thông tin nội bộ.
Ví dụ response dành cho danh sách:
{
"data": [
{
"id": 125,
"name": "Máy lọc nước",
"price": 6500000,
"thumbnail": "/uploads/product-125.jpg"
}
]
}
Đây là tư duy quan trọng khi thiết kế API: response nên phục vụ nhu cầu của client, không phải sao chép nguyên trạng một bản ghi database.
Mã trạng thái HTTP và cách sử dụng đúng trong API
Status code giúp client hiểu kết quả xử lý request mà không cần đọc toàn bộ nội dung response. Sử dụng đúng status code khiến API dễ tích hợp và dễ debug hơn.
| Mã | Ý nghĩa thường gặp |
|---|---|
| 200 | Request thành công và server trả về nội dung. |
| 201 | Resource mới đã được tạo. |
| 204 | Request thành công nhưng không có nội dung response cần trả về. |
| 400 | Request không hợp lệ. |
| 401 | Chưa xác thực hoặc thông tin xác thực không hợp lệ. |
| 403 | Đã xác thực nhưng không có quyền thực hiện. |
| 404 | Không tìm thấy resource. |
| 409 | Xung đột với trạng thái hiện tại của resource. |
| 422 | Dữ liệu có cấu trúc phù hợp nhưng không đáp ứng điều kiện nghiệp vụ hoặc validation. |
| 429 | Client gửi quá nhiều request trong khoảng thời gian nhất định. |
| 500 | Lỗi không mong muốn phía server. |
401 và 403 không giống nhau
Đây là hai mã rất dễ bị sử dụng nhầm.
401 Unauthorized thường liên quan đến việc client chưa cung cấp thông tin xác thực hợp lệ. Ví dụ token bị thiếu hoặc không hợp lệ.
403 Forbidden thường có nghĩa server đã nhận diện client nhưng client không được phép thực hiện thao tác đó.
Ví dụ một người dùng đã đăng nhập nhưng cố xóa tài khoản của người khác mà không có quyền quản trị thì có thể nhận 403.
404 không chỉ dành cho URL sai
404 thường được sử dụng khi resource được yêu cầu không tồn tại hoặc không thể được cung cấp trong ngữ cảnh của endpoint.
Ví dụ:
GET /api/products/999999
Nếu sản phẩm này không tồn tại, server có thể trả:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "PRODUCT_NOT_FOUND",
"message": "Không tìm thấy sản phẩm."
}
}
Thông báo lỗi nên giúp client hiểu vấn đề nhưng không nên tiết lộ thông tin nội bộ như câu SQL, đường dẫn server hoặc stack trace.
Validation và xử lý lỗi nên được thiết kế ngay từ đầu
Một API tốt không chỉ xử lý request hợp lệ mà còn phải dự đoán những request sai. Client có thể gửi thiếu trường bắt buộc, sai kiểu dữ liệu, giá trị nằm ngoài giới hạn hoặc tham chiếu tới resource không tồn tại.
Ví dụ API yêu cầu tên sản phẩm không được để trống và giá phải lớn hơn 0. Request:
POST /api/products
Content-Type: application/json
{
"name": "",
"price": -500
}
Response có thể mô tả lỗi theo cấu trúc nhất quán:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Dữ liệu không hợp lệ.",
"fields": {
"name": "Tên sản phẩm không được để trống.",
"price": "Giá sản phẩm phải lớn hơn 0."
}
}
}
Cách làm này tốt hơn việc trả một chuỗi lỗi chung chung như Something went wrong, bởi frontend có thể xác định chính xác trường nào cần hiển thị thông báo.
Không trả lỗi database trực tiếp cho client
Đây là nguyên tắc đặc biệt quan trọng đối với backend PHP và các hệ thống sử dụng database.
Không nên để client nhận những thông tin như:
SQLSTATE[23000]: Integrity constraint violation...
Table 'website_products'...
/var/www/html/api/ProductController.php:125
Những thông tin này có thể hữu ích khi debug nội bộ nhưng không nên xuất hiện trong response production. API nên ghi lỗi chi tiết vào hệ thống log phía server và trả về thông báo an toàn, có mã lỗi đủ rõ để đội phát triển tra cứu.
Ví dụ:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Đã xảy ra lỗi khi xử lý yêu cầu."
}
}
Nhờ đó, frontend không phải phụ thuộc vào thông báo lỗi nội bộ và hệ thống cũng hạn chế nguy cơ làm lộ cấu trúc database.
Xác thực và phân quyền cho REST API
Khi API chỉ phục vụ dữ liệu công khai, client có thể gọi trực tiếp một số endpoint mà không cần đăng nhập. Nhưng với tài khoản, đơn hàng, thông tin cá nhân hoặc khu vực quản trị, server phải biết ai đang gửi request và người đó được phép làm gì.
Hai khái niệm cần tách biệt là authentication và authorization. Authentication xác định danh tính của client hoặc người dùng. Authorization quyết định danh tính đó có quyền thực hiện hành động đang yêu cầu hay không.
Ví dụ một người dùng có thể được phép xem đơn hàng của chính mình nhưng không được phép xem đơn hàng của tài khoản khác. Người quản trị có thể có thêm quyền sửa hoặc xóa dữ liệu.
Bearer Token trong request
Một cách phổ biến để truyền thông tin xác thực là sử dụng Authorization header với Bearer Token:
GET /api/orders
Host: example.com
Accept: application/json
Authorization: Bearer YOUR_TOKEN
Server đọc token, xác minh token và xác định danh tính cũng như quyền của client. Token không nên được đặt trong query string nếu không thực sự cần thiết, bởi URL có thể xuất hiện trong lịch sử trình duyệt, log máy chủ, proxy hoặc các hệ thống theo dõi.
Toàn bộ quá trình truyền thông tin xác thực cũng cần được bảo vệ bằng HTTPS. Việc sử dụng token mà không có kết nối mã hóa có thể khiến thông tin xác thực bị đánh cắp trên đường truyền.
Không đồng nhất đăng nhập với quyền truy cập
Một người dùng đăng nhập thành công chưa có nghĩa là họ được phép gọi mọi endpoint.
Ví dụ API có thể kiểm tra lần lượt:
- Token có hợp lệ hay không.
- Token thuộc về người dùng nào.
- Tài khoản có đang hoạt động hay không.
- Người dùng có quyền truy cập resource hay không.
- Thao tác cụ thể có nằm trong phạm vi quyền được cấp hay không.
Cách kiểm tra nhiều lớp này đặc biệt cần thiết đối với các API quản trị, thương mại điện tử và hệ thống có nhiều nhóm người dùng.
Phân trang, tìm kiếm và lọc dữ liệu trong collection
Không nên trả toàn bộ dữ liệu khi collection có thể chứa hàng nghìn hoặc hàng triệu bản ghi. Một request đơn giản như GET /api/products có thể trở thành vấn đề hiệu năng nếu server phải truy vấn và trả về hàng trăm nghìn sản phẩm.
Giải pháp phổ biến là phân trang:
GET /api/products?page=2&limit=20
Trong ví dụ này, client yêu cầu trang thứ hai với tối đa 20 bản ghi.
Response có thể cung cấp cả dữ liệu và thông tin phân trang:
{
"data": [
{
"id": 121,
"name": "Sản phẩm A"
},
{
"id": 122,
"name": "Sản phẩm B"
}
],
"pagination": {
"page": 2,
"limit": 20,
"total": 145,
"total_pages": 8
}
}
Cấu trúc cụ thể có thể thay đổi tùy dự án. Điều quan trọng là client có đủ thông tin để xây dựng giao diện phân trang mà không phải tự đoán.
Filtering và sorting
Query parameter cũng rất phù hợp cho việc lọc và sắp xếp:
GET /api/products?category_id=8&status=active
Hoặc:
GET /api/products?sort=price&order=asc
Khi API hỗ trợ tìm kiếm:
GET /api/products?q=may%20loc%20nuoc
Server nên kiểm soát danh sách trường được phép lọc và sắp xếp. Không nên lấy trực tiếp giá trị client gửi rồi nối vào câu SQL. Việc này vừa gây khó kiểm soát nghiệp vụ vừa có thể tạo ra lỗ hổng SQL injection nếu tầng truy vấn không được bảo vệ.
Offset pagination và cursor pagination
Phân trang bằng page và limit dễ triển khai và phù hợp với nhiều website:
GET /api/products?page=5&limit=20
Tuy nhiên, với collection rất lớn hoặc dữ liệu liên tục thay đổi, offset pagination có thể trở nên kém hiệu quả khi database phải bỏ qua một lượng lớn bản ghi.
Khi đó có thể sử dụng cursor pagination. Client nhận một cursor từ response trước rồi sử dụng cursor đó để lấy nhóm dữ liệu tiếp theo:
GET /api/products?limit=20&cursor=eyJpZCI6MTI1fQ
Cursor không nhất thiết phải chứa ID trực tiếp. Nó có thể là một giá trị được mã hóa hoặc ký bởi server. Cách này thường phù hợp hơn với các feed, danh sách lớn hoặc hệ thống có dữ liệu thay đổi liên tục.
Versioning giúp API không phá vỡ client cũ
API sau khi đưa vào sử dụng rất khó thay đổi tùy tiện. Có thể hôm nay chỉ có một website sử dụng API, nhưng sau một thời gian API có thể được ứng dụng di động, hệ thống đối tác hoặc phần mềm nội bộ sử dụng.
Nếu thay đổi cấu trúc response mà không có chiến lược tương thích, client cũ có thể ngừng hoạt động.
Một cách phổ biến là version trong URL:
/api/v1/products
/api/v2/products
Khi có thay đổi lớn về contract, server có thể duy trì v1 trong một khoảng thời gian trong khi client chuyển dần sang v2.
Versioning không nên được sử dụng như một lý do để thay đổi API liên tục. Trước khi tạo version mới, nên xem xét liệu thay đổi đó có thể thực hiện theo hướng tương thích ngược hay không.
Thay đổi nào có thể phá vỡ client?
Một số thay đổi tưởng như nhỏ nhưng có thể gây lỗi cho client:
- Xóa một trường đang được client sử dụng.
- Đổi kiểu dữ liệu của một trường.
- Đổi ý nghĩa của một giá trị.
- Đổi cấu trúc JSON.
- Thay đổi quy tắc validation.
- Thay đổi status code mà client đang dựa vào.
- Thay đổi cách phân trang.
Ngược lại, việc bổ sung một trường mới thường ít gây vấn đề hơn nếu client được thiết kế để bỏ qua những trường mà nó không sử dụng.
Vì vậy, API contract nên được xem là một cam kết giữa server và các client, không phải một chi tiết nội bộ của backend.
Rate limiting và bảo vệ API trước lượng request bất thường
Một API công khai có thể bị gọi hàng nghìn hoặc hàng triệu lần nếu không có giới hạn phù hợp. Nguyên nhân có thể là người dùng sử dụng quá mức, crawler, bot, lỗi vòng lặp ở frontend hoặc hành vi tấn công.
Rate limiting cho phép server giới hạn số request mà một client được phép thực hiện trong một khoảng thời gian.
Ví dụ một API có thể quy định:
100 requests / minute
Con số thực tế phải được lựa chọn dựa trên đặc điểm của hệ thống. API tìm kiếm, API đăng nhập và API quản trị có thể cần các giới hạn khác nhau.
Khi vượt giới hạn, server có thể sử dụng status code 429 Too Many Requests và cung cấp thông tin phù hợp để client biết nên thử lại khi nào.
Rate limiting không thay thế các biện pháp bảo mật khác. Nó chỉ là một lớp kiểm soát lưu lượng trong tổng thể hệ thống.
Caching và tối ưu hiệu năng cho các API có lượng truy cập lớn
Không phải request nào cũng cần truy cập database. Những dữ liệu ít thay đổi nhưng được đọc thường xuyên là ứng viên tốt cho caching.
Ví dụ danh sách danh mục:
GET /api/categories
Nếu danh mục chỉ thay đổi vài lần mỗi ngày nhưng được hàng nghìn request truy cập, việc truy vấn database cho từng request là không cần thiết.
Cache có thể được triển khai ở nhiều lớp khác nhau như browser, reverse proxy, CDN hoặc hệ thống cache phía server. Mỗi lớp có phạm vi và mục đích khác nhau.
HTTP caching cần được thiết kế theo dữ liệu
Header HTTP có thể cung cấp thông tin về chính sách cache. Ví dụ:
Cache-Control: public, max-age=300
Điều này cho phép response được cache trong khoảng thời gian nhất định theo chính sách được áp dụng.
Đối với dữ liệu cá nhân hoặc dữ liệu phụ thuộc vào quyền của từng người dùng, cần đặc biệt cẩn thận. Một response dành cho người dùng A không được vô tình được cache rồi trả cho người dùng B.
Vì vậy, tối ưu cache không đơn giản là đặt một thời gian cache thật dài. Cần xác định rõ dữ liệu nào có thể chia sẻ, dữ liệu nào phụ thuộc người dùng và khi nào cache phải được làm mới.
Bảo mật REST API từ tầng request đến dữ liệu
Bảo mật API cần được xem xét ngay từ lúc thiết kế thay vì chờ đến khi hệ thống hoàn thành mới bổ sung. Một endpoint hoạt động đúng về mặt nghiệp vụ nhưng thiếu kiểm soát quyền truy cập vẫn có thể trở thành điểm yếu nghiêm trọng.
Luôn kiểm tra dữ liệu đầu vào
Client không phải nguồn dữ liệu đáng tin cậy. Mọi dữ liệu gửi lên server đều phải được validation.
Ví dụ với một trường ID, server cần kiểm tra kiểu dữ liệu và sự tồn tại của resource. Với giá sản phẩm, cần kiểm tra kiểu số, giới hạn giá trị và các quy tắc nghiệp vụ liên quan.
Validation nên được thực hiện ở server ngay cả khi frontend đã kiểm tra dữ liệu. Kiểm tra phía frontend chủ yếu phục vụ trải nghiệm người dùng, còn kiểm tra phía server là lớp bảo vệ thực sự.
Ngăn SQL injection bằng truy vấn có tham số
Không nên ghép trực tiếp dữ liệu người dùng vào câu SQL:
$sql = "SELECT * FROM products WHERE id = " . $_GET['id'];
Cách an toàn hơn là sử dụng prepared statement hoặc cơ chế parameter binding của thư viện database:
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = :id");
$stmt->execute(['id' => $id]);
Ngoài việc sử dụng parameter binding, backend vẫn phải kiểm tra kiểu dữ liệu và quy tắc nghiệp vụ. Prepared statement là một lớp bảo vệ quan trọng nhưng không thay thế toàn bộ quy trình validation.
Không để lộ thông tin nội bộ
Response production không nên chứa stack trace, tên bảng database, đường dẫn thư mục trên server, câu SQL hoặc thông tin cấu hình.
Thay vì:
{
"error": "/var/www/html/app/Models/Product.php line 125: SQLSTATE..."
}
Nên trả một thông tin có chủ đích:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Đã xảy ra lỗi khi xử lý yêu cầu."
}
}
Chi tiết kỹ thuật nên được ghi vào log nội bộ để lập trình viên có thể điều tra.
Thiết kế cấu trúc lỗi có thể sử dụng lâu dài
Khi API lớn dần, lỗi không nên chỉ là những câu thông báo tự do. Một mã lỗi ổn định giúp frontend, mobile app và hệ thống tích hợp có thể xử lý lỗi theo chương trình.
Ví dụ:
{
"error": {
"code": "EMAIL_ALREADY_EXISTS",
"message": "Email đã được sử dụng.",
"details": {}
}
}
Client có thể dựa vào code thay vì phân tích chuỗi message. Message có thể thay đổi ngôn ngữ hoặc cách diễn đạt mà không làm hỏng logic của frontend.
Với lỗi validation nhiều trường:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Dữ liệu không hợp lệ.",
"fields": {
"email": "Email không đúng định dạng.",
"password": "Mật khẩu chưa đáp ứng yêu cầu."
}
}
}
Đây là cách tổ chức đặc biệt hữu ích với form đăng ký, đặt hàng, cập nhật hồ sơ và các API có nhiều trường nhập liệu.
REST API trong PHP nên được tổ chức như thế nào?
PHP có thể xây dựng REST API từ mức rất đơn giản đến kiến trúc lớn. Với một dự án nhỏ, có thể bắt đầu từ một router và các lớp xử lý request. Khi hệ thống phát triển, nên tách trách nhiệm để tránh việc một file PHP vừa đọc request, vừa truy vấn database, vừa xử lý nghiệp vụ và vừa tạo JSON response.
Một cách tổ chức phổ biến có thể chia thành các lớp:
- Router: xác định endpoint và method.
- Controller: tiếp nhận request và điều phối xử lý.
- Service: chứa logic nghiệp vụ.
- Repository hoặc Model: giao tiếp với database.
- Validator: kiểm tra dữ liệu đầu vào.
- Authentication và Authorization: xác thực và kiểm tra quyền.
- Response layer: chuẩn hóa response.
Ví dụ một request:
POST /api/products
có thể đi qua luồng:
- Router nhận POST /api/products.
- Controller đọc JSON body.
- Validator kiểm tra dữ liệu.
- Authentication xác định người gửi.
- Authorization kiểm tra quyền tạo sản phẩm.
- Service xử lý nghiệp vụ.
- Repository lưu dữ liệu.
- Controller tạo response 201.
Cách phân lớp này giúp từng thành phần có nhiệm vụ rõ ràng và dễ kiểm thử hơn.
Không nên để Controller chứa toàn bộ nghiệp vụ
Một Controller dài hàng nghìn dòng thường là dấu hiệu kiến trúc đang bị dồn trách nhiệm vào một nơi.
Ví dụ xử lý tạo đơn hàng có thể bao gồm kiểm tra tồn kho, tính phí vận chuyển, áp dụng mã giảm giá, tính thuế, tạo giao dịch và gửi thông báo. Nếu toàn bộ logic nằm trong Controller, việc thay đổi một quy tắc nhỏ cũng có thể ảnh hưởng đến nhiều phần khác.
Tách nghiệp vụ vào Service giúp Controller chỉ đóng vai trò điều phối. Điều này cũng thuận lợi khi cùng một nghiệp vụ cần được gọi từ API, cron job hoặc một quy trình backend khác.
Những sai lầm thường gặp khi xây dựng REST API
Nhiều API chạy được nhưng vẫn khó bảo trì vì những quyết định thiết kế ban đầu thiếu nhất quán. Một số lỗi xuất hiện rất thường xuyên.
- Dùng POST cho tất cả thao tác.
- Đưa quá nhiều động từ vào URL.
- Không thống nhất cấu trúc JSON response.
- Trả status code không phản ánh đúng kết quả.
- Trả lỗi database trực tiếp cho client.
- Không validation dữ liệu phía server.
- Không kiểm tra quyền truy cập resource.
- Trả toàn bộ database record dù client không cần.
- Không có chiến lược phân trang cho collection lớn.
- Đổi response tùy tiện khiến client cũ bị lỗi.
- Đặt token trong URL.
- Không giới hạn request đối với endpoint nhạy cảm.
Không phải mọi API đều phải áp dụng một khuôn mẫu giống nhau. Điều quan trọng là những quyết định được lựa chọn phải có lý do, được ghi nhận thành quy ước và được áp dụng nhất quán trong toàn bộ hệ thống.
Checklist thiết kế REST API trước khi đưa vào sử dụng
Trước khi công bố một API cho frontend, ứng dụng di động hoặc đối tác bên ngoài, có thể kiểm tra lần lượt các nhóm vấn đề sau:
- Resource đã được mô hình hóa rõ ràng chưa?
- Endpoint có tên nhất quán và dễ đoán không?
- HTTP method có được sử dụng đúng ngữ nghĩa không?
- Path parameter và query parameter có được phân chia hợp lý không?
- Request body có validation đầy đủ không?
- Response có cấu trúc thống nhất không?
- Status code có phản ánh đúng kết quả không?
- Lỗi có mã riêng để client xử lý không?
- API có xác thực và phân quyền phù hợp không?
- Resource có được kiểm tra quyền sở hữu hoặc quyền truy cập không?
- Collection lớn đã có pagination chưa?
- Filtering và sorting có giới hạn trường được phép sử dụng không?
- API có rate limiting đối với endpoint phù hợp không?
- Dữ liệu nhạy cảm có được bảo vệ bằng HTTPS không?
- Response có vô tình làm lộ thông tin nội bộ không?
- Database query có sử dụng parameter binding không?
- API có chiến lược versioning khi contract thay đổi không?
- Những endpoint đọc dữ liệu có cần caching không?
- Hệ thống có log đủ để điều tra lỗi nhưng không ghi lại secret không?
- Client cũ có tiếp tục hoạt động sau các thay đổi hay không?
REST API tốt không được đánh giá chỉ bằng việc request trả về dữ liệu thành công. Một API đáng tin cậy phải có contract rõ ràng, cách đặt tên nhất quán, status code hợp lý, xử lý lỗi có chủ đích, kiểm soát quyền truy cập, bảo mật dữ liệu và khả năng mở rộng khi hệ thống lớn lên.
Đối với Web Mới, việc xây dựng API bằng PHP có thể bắt đầu từ những endpoint CRUD đơn giản nhưng nên hình thành quy ước ngay từ đầu. Khi dự án phát triển, các lớp validation, authentication, authorization, service, repository, caching, rate limiting và versioning có thể được bổ sung theo đúng nhu cầu thay vì làm mọi thứ phức tạp ngay từ ngày đầu.
Điểm cốt lõi cần nhớ là REST không phải một bộ quy tắc để học thuộc. REST là một cách tư duy về tài nguyên, trạng thái và giao tiếp giữa các hệ thống. Khi hiểu được tư duy đó, việc thiết kế endpoint, lựa chọn HTTP method hay xử lý response sẽ trở nên tự nhiên hơn rất nhiều.
- 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 *