HTTP API là gì? Toàn bộ kiến thức HTTP cần biết khi làm API
Bùi Tấn Lực
- 107
- 05/09/2026
Khi xây dựng một website hiện đại, API thường là lớp trung gian giúp trình duyệt, ứng dụng di động, hệ thống quản trị hoặc một dịch vụ khác trao đổi dữ liệu với máy chủ. Nhưng phía sau một API tưởng như đơn giản lại là cả một cơ chế giao tiếp dựa trên HTTP: client gửi request, server xử lý và trả về response.
Nếu chỉ biết gửi một URL và nhận JSON, bạn có thể sử dụng API ở mức cơ bản. Nhưng để thiết kế API tốt, xử lý lỗi chính xác, bảo mật, tối ưu tốc độ và debug những tình huống khó, cần hiểu HTTP ở mức sâu hơn: method hoạt động ra sao, header dùng để làm gì, status code mang ý nghĩa gì, body được truyền như thế nào, cache tác động ra sao và tại sao cùng một request nhưng server có thể phản hồi khác nhau.
Đối với Web Mới, HTTP không chỉ là kiến thức lý thuyết. Đây là nền tảng rất quan trọng khi xây dựng website bằng PHP, JavaScript, AJAX hoặc các hệ thống API phục vụ nhiều loại client khác nhau.
Bài viết này tập trung vào phần HTTP mà một lập trình viên web thực sự cần khi làm API, đi từ bản chất của giao tiếp HTTP đến cách đọc và xây dựng một HTTP request/response đúng cách.

HTTP và API có quan hệ với nhau như thế nào?
HTTP và API không phải là hai khái niệm đồng nghĩa.
HTTP là một giao thức dùng để các hệ thống trao đổi dữ liệu trên mạng. Trong môi trường web, HTTP quy định cách client gửi yêu cầu và server trả kết quả.
API là một giao diện được thiết kế để phần mềm có thể giao tiếp với phần mềm khác. API có thể sử dụng HTTP, nhưng về mặt khái niệm API rộng hơn HTTP.
Ví dụ, một website có thể cung cấp API để lấy danh sách sản phẩm. JavaScript trên trình duyệt gửi HTTP request đến API, máy chủ PHP nhận request, truy vấn cơ sở dữ liệu rồi trả dữ liệu về dưới dạng JSON.
GET /api/products HTTP/1.1
Host: example.com
Accept: application/json
Máy chủ có thể trả về:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": [
{
"id": 1,
"name": "Sản phẩm A"
}
]
}
Trong ví dụ này, HTTP cung cấp cơ chế vận chuyển và quy tắc giao tiếp, còn API quy định endpoint nào tồn tại, dữ liệu có cấu trúc ra sao và client được phép thực hiện những thao tác nào.
Đây là điểm cần phân biệt ngay từ đầu. Nếu hiểu API chỉ là một URL trả JSON, bạn sẽ rất dễ bỏ qua nhiều thành phần quan trọng của HTTP.
Mô hình giao tiếp request và response
Phần lớn API web hoạt động theo mô hình client-server. Client tạo một HTTP request gửi đến server. Server tiếp nhận request, thực hiện xử lý và tạo HTTP response trả về.
Một request thường gồm các thành phần chính:
- Method: xác định loại thao tác mà client muốn thực hiện.
- URL: xác định tài nguyên hoặc endpoint cần truy cập.
- Header: truyền thông tin bổ sung về request.
- Body: chứa dữ liệu gửi lên server nếu request cần dữ liệu.
Response thường gồm:
- Status code: cho biết kết quả xử lý ở mức tổng quát.
- Header: mô tả response và các chính sách liên quan.
- Body: chứa dữ liệu mà server trả về.
Có thể hình dung toàn bộ quá trình như sau:
Client → HTTP Request → Server → xử lý nghiệp vụ → HTTP Response → Client
Ví dụ khi người dùng đăng nhập, trình duyệt có thể gửi thông tin tài khoản đến API:
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"email": "user@example.com",
"password": "123456"
}
Nếu thông tin hợp lệ, server có thể phản hồi:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"message": "Đăng nhập thành công"
}
Điểm quan trọng là server không trả về kết quả một cách tùy ý. HTTP có cấu trúc riêng cho request và response. API được xây dựng tốt thường tận dụng những quy tắc này thay vì tự tạo ra một giao thức riêng không cần thiết.
Cấu trúc của một HTTP request
Muốn làm API chắc tay, trước tiên cần đọc được một request ở dạng thô.
Một HTTP request cơ bản có thể được biểu diễn như sau:
POST /api/products?page=2 HTTP/1.1
Host: example.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer token-example
{
"name": "Bàn làm việc",
"price": 2500000
}
Request trên có thể chia thành bốn phần.
Request line
Dòng đầu tiên chứa method, target và phiên bản HTTP.
POST /api/products?page=2 HTTP/1.1
POST là method. Phần /api/products?page=2 là target mà client muốn truy cập. Phần HTTP/1.1 cho biết phiên bản giao thức được sử dụng.
Request header
Các dòng tiếp theo cung cấp thông tin bổ sung.
Host: example.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer token-example
Header không phải dữ liệu nghiệp vụ chính. Nó giống như phần thông tin mô tả cách request nên được tiếp nhận hoặc xử lý.
Request body
Body là phần dữ liệu được gửi trong request. Không phải method nào cũng bắt buộc phải có body.
Ví dụ khi tạo sản phẩm:
{
"name": "Bàn làm việc",
"price": 2500000
}
Server cần biết body đang được gửi theo định dạng nào. Vì vậy header Content-Type đóng vai trò rất quan trọng.
Cấu trúc của HTTP response
Response cũng có cấu trúc rõ ràng. Ví dụ:
HTTP/1.1 201 Created
Content-Type: application/json
{
"success": true,
"message": "Tạo sản phẩm thành công",
"data": {
"id": 15,
"name": "Bàn làm việc",
"price": 2500000
}
}
Dòng đầu tiên là status line. Sau đó là các response header, tiếp theo có thể là response body.
Status line
Ví dụ:
HTTP/1.1 201 Created
Trong đó 201 là status code và Created là phần mô tả trạng thái.
Status code giúp client hiểu kết quả tổng quát mà không cần đọc nội dung body. Đây là lý do API nên sử dụng status code một cách nhất quán thay vì trả mọi trường hợp bằng 200.
Response header
Ví dụ:
Content-Type: application/json
Header này cho client biết nội dung response được cung cấp dưới dạng JSON.
Response body
Body thường chứa dữ liệu hoặc thông tin lỗi mà client cần sử dụng.
{
"success": true,
"message": "Tạo sản phẩm thành công"
}
Đối với API hiện đại, JSON là định dạng phổ biến vì dễ xử lý bằng JavaScript, PHP và nhiều ngôn ngữ lập trình khác.
URL, URI và endpoint trong API
Trong quá trình làm API, các khái niệm URL, URI và endpoint thường xuất hiện cùng nhau nhưng không hoàn toàn giống nhau.
URI là khái niệm rộng dùng để định danh một tài nguyên. URL là một dạng URI có khả năng chỉ ra cách truy cập tài nguyên đó. Trong công việc web hằng ngày, lập trình viên thường gọi địa chỉ API là URL.
Ví dụ:
https://example.com/api/products/25
Trong thiết kế API, endpoint thường được dùng để nói đến điểm mà client có thể gọi để thực hiện một chức năng hoặc truy cập một tài nguyên.
Ví dụ:
GET /api/products
GET /api/products/25
POST /api/products
PUT /api/products/25
DELETE /api/products/25
Những dòng trên có cùng nhóm tài nguyên nhưng khác method và có thể đại diện cho những thao tác khác nhau.
Path và query string
URL có thể chứa phần path và query string.
/api/products/25?sort=price&page=2
Trong ví dụ này, 25 nằm trong path và thường được dùng để xác định một sản phẩm cụ thể. Phần sort=price&page=2 là query string và thường được dùng cho lọc, sắp xếp, tìm kiếm hoặc phân trang.
Đây là một phân biệt quan trọng khi thiết kế API. ID của tài nguyên thường nên nằm ở path, trong khi các tham số thay đổi cách truy vấn danh sách thường phù hợp với query string.
Các HTTP method quan trọng khi xây dựng API
HTTP cung cấp nhiều method, nhưng khi xây dựng API web, một số method xuất hiện thường xuyên hơn hẳn.
| Method | Mục đích thường gặp | Ví dụ |
|---|---|---|
| GET | Lấy dữ liệu | /api/products |
| POST | Tạo tài nguyên hoặc yêu cầu xử lý | /api/products |
| PUT | Cập nhật toàn bộ hoặc thay thế tài nguyên | /api/products/25 |
| PATCH | Cập nhật một phần tài nguyên | /api/products/25 |
| DELETE | Xóa tài nguyên | /api/products/25 |
Việc chọn method không chỉ mang tính hình thức. Client, proxy, cache và nhiều công cụ HTTP có thể dựa vào ý nghĩa chuẩn của method để xử lý request.
GET dùng để lấy dữ liệu
GET thường được sử dụng khi client muốn đọc tài nguyên.
GET /api/products
Hoặc lấy một sản phẩm:
GET /api/products/25
Dữ liệu lọc và phân trang thường được truyền qua query string:
GET /api/products?category=office&page=2&limit=20
GET không nên được thiết kế để thực hiện thao tác thay đổi dữ liệu như xóa bản ghi, cập nhật giá hoặc tạo đơn hàng.
POST thường dùng để tạo hoặc thực hiện một hành động
POST thường được dùng khi server cần tiếp nhận dữ liệu mới hoặc thực hiện một thao tác mà kết quả phụ thuộc vào request.
POST /api/products
Body có thể chứa:
{
"name": "Ghế văn phòng",
"price": 1800000
}
POST cũng thường xuất hiện ở các API hành động như đăng nhập, gửi biểu mẫu, tạo đơn hàng hoặc upload dữ liệu.
PUT và PATCH không giống nhau
Hai method này đều liên quan đến cập nhật nhưng cách sử dụng có ý nghĩa khác nhau.
PUT thường được hiểu là thay thế biểu diễn của tài nguyên bằng dữ liệu được gửi lên.
PUT /api/products/25
PATCH phù hợp hơn khi chỉ thay đổi một phần:
PATCH /api/products/25
Ví dụ chỉ thay đổi giá:
{
"price": 2200000
}
Trong API thực tế, việc lựa chọn PUT hay PATCH cần thống nhất với cách server xử lý dữ liệu. Không nên chỉ chọn theo thói quen.
DELETE dùng để xóa tài nguyên
DELETE /api/products/25
Server có thể trả về trạng thái xác nhận thao tác đã được thực hiện.
HTTP/1.1 204 No Content
Trường hợp này response không cần body.
Safe, idempotent và tại sao lập trình viên API cần hiểu
Đây là phần thường bị bỏ qua khi học API nhưng lại rất quan trọng khi thiết kế hệ thống thực tế.
Một HTTP method có thể có các đặc tính như safe và idempotent. Hai khái niệm này giúp mô tả tác động của việc gửi request nhiều lần.
Safe method
Một method được xem là safe khi client không yêu cầu server thực hiện thay đổi trạng thái tài nguyên thông qua request đó.
GET là ví dụ điển hình. Việc gọi:
GET /api/products/25
về mặt ngữ nghĩa là đọc sản phẩm, không phải yêu cầu thay đổi sản phẩm.
Điều này không có nghĩa server tuyệt đối không thể ghi log, cập nhật thống kê hoặc thực hiện một tác động nội bộ nào đó. Ý nghĩa safe tập trung vào hành động mà client yêu cầu đối với tài nguyên.
Idempotent method
Một request có tính idempotent nếu việc thực hiện cùng một request nhiều lần có cùng tác động mong muốn lên trạng thái tài nguyên như thực hiện một lần.
Ví dụ một request PUT thiết lập sản phẩm thành một trạng thái xác định:
PUT /api/products/25
{
"name": "Bàn làm việc",
"price": 2500000
}
Nếu request được gửi lại nhiều lần và cuối cùng tài nguyên vẫn có cùng trạng thái, thao tác đó có tính idempotent.
Ngược lại, POST tạo đơn hàng thường không thể mặc nhiên xem là idempotent. Nếu client gửi cùng request hai lần, hệ thống có thể tạo hai đơn hàng.
Điểm này trở nên đặc biệt quan trọng khi mạng không ổn định. Client có thể không biết request đã đến server hay chưa và quyết định gửi lại. Nếu API không có chiến lược chống tạo trùng, một thao tác tưởng như chỉ được thực hiện một lần có thể xảy ra nhiều lần.
HTTP không phải chỉ là một URL trả JSON
Một API tốt không được đánh giá chỉ bằng việc nó trả về JSON.
Hai API đều có thể trả JSON nhưng chất lượng hoàn toàn khác nhau:
HTTP/1.1 200 OK
{
"error": "Không tìm thấy sản phẩm"
}
và:
HTTP/1.1 404 Not Found
{
"error": "Không tìm thấy sản phẩm"
}
Response thứ hai truyền tải nhiều thông tin có giá trị hơn ở tầng HTTP. Client có thể dựa vào status code để phân biệt tài nguyên không tồn tại với một request thành công.
Đây cũng là tư duy quan trọng khi xây dựng API tại Web Mới: dữ liệu nghiệp vụ nằm trong body, còn HTTP nên được sử dụng đúng vai trò để mô tả trạng thái của quá trình giao tiếp.
Khi đã hiểu request, response, URL và method, bước tiếp theo là đi sâu vào phần thường quyết định API có được thiết kế đúng hay không: HTTP header, body, Content-Type, Accept và status code.
HTTP header quyết định cách dữ liệu được hiểu
Nếu request và response là phần khung của quá trình giao tiếp thì header chính là lớp thông tin mô tả cách hai bên nên hiểu và xử lý dữ liệu. Khi làm API, rất nhiều lỗi tưởng như nằm ở PHP, JavaScript hoặc cơ sở dữ liệu nhưng thực tế lại bắt nguồn từ header không đúng.
Header là tập hợp các trường có dạng tên và giá trị:
Content-Type: application/json
Accept: application/json
Mỗi header có một mục đích riêng. Có header mô tả dữ liệu, có header liên quan đến xác thực, cache, ngôn ngữ, nguồn request hoặc điều kiện xử lý.
Không nên xem header là những dòng bắt buộc phải sao chép từ một ví dụ trên Internet. Khi hiểu ý nghĩa của từng header, lập trình viên sẽ biết khi nào cần sử dụng, khi nào không cần và tránh việc thêm quá nhiều thông tin không cần thiết.
Host
Host xác định máy chủ mà client muốn giao tiếp.
Host: webmoi.vn
Trong môi trường một máy chủ phục vụ nhiều domain, thông tin này đặc biệt quan trọng vì server cần biết request đang hướng tới hostname nào.
Accept
Accept cho biết client mong muốn nhận kiểu dữ liệu nào từ server.
Accept: application/json
Ví dụ một client gọi API và yêu cầu JSON. Server có thể dựa trên thông tin này để lựa chọn cách biểu diễn response phù hợp.
Điểm cần nhớ là Accept mô tả dữ liệu client muốn nhận, không phải dữ liệu client đang gửi.
Content-Type
Content-Type mô tả kiểu dữ liệu đang nằm trong body của message.
Nếu client gửi JSON:
Content-Type: application/json
Nếu gửi dữ liệu biểu mẫu:
Content-Type: application/x-www-form-urlencoded
Nếu upload file cùng các trường biểu mẫu, thường sử dụng:
Content-Type: multipart/form-data
Đây là một trong những header quan trọng nhất khi làm API vì server cần biết cách đọc body. Nếu JavaScript gửi JSON nhưng server lại cố đọc dữ liệu như form POST truyền thống, kết quả có thể không như mong đợi.
Authorization
Authorization thường được sử dụng để truyền thông tin xác thực.
Authorization: Bearer your-token
Từ khóa Bearer cho biết giá trị phía sau là một bearer token theo cách sử dụng phổ biến của HTTP API.
Không nên nhầm Authorization với việc HTTP tự động xác thực người dùng. HTTP chỉ cung cấp cơ chế truyền thông tin; việc token có hợp lệ hay không vẫn phải được ứng dụng kiểm tra.
Body và các định dạng dữ liệu thường gặp
Body là nơi chứa dữ liệu của request hoặc response. HTTP không bắt buộc body phải là JSON. Định dạng phụ thuộc vào mục đích giao tiếp và Content-Type.
Trong API web, ba dạng dữ liệu rất thường gặp là JSON, form-urlencoded và multipart/form-data.
JSON
JSON phù hợp với các API trao đổi dữ liệu có cấu trúc.
{
"name": "Nguyễn Văn A",
"age": 30,
"active": true
}
Request tương ứng:
POST /api/users HTTP/1.1
Content-Type: application/json
{
"name": "Nguyễn Văn A",
"age": 30,
"active": true
}
Với PHP, cần chú ý rằng JSON request body không tự động xuất hiện trong $_POST giống dữ liệu form thông thường.
Thông thường có thể đọc raw body:
<?php
$body = file_get_contents('php://input');
$data = json_decode($body, true);
?>
Sau khi giải mã, biến $data có thể chứa dữ liệu dạng mảng PHP.
application/x-www-form-urlencoded
Đây là dạng dữ liệu quen thuộc với form HTML truyền thống.
name=Nguyen+Van+A&age=30
Header:
Content-Type: application/x-www-form-urlencoded
Với PHP, loại request này thường được ánh xạ thuận tiện vào $_POST.
Điểm khác biệt với JSON rất quan trọng. Nếu API được thiết kế nhận JSON nhưng client gửi form-urlencoded, server cần có chiến lược xử lý rõ ràng thay vì cố đoán định dạng.
multipart/form-data
multipart/form-data thường được sử dụng khi request cần gửi file cùng các trường dữ liệu.
Content-Type: multipart/form-data
Ví dụ một API nhận ảnh sản phẩm có thể cần các trường:
name=Ghế văn phòng
price=1800000
image=product.jpg
Trình duyệt hoặc thư viện HTTP sẽ tự xây dựng cấu trúc multipart thực tế, vì vậy khi sử dụng JavaScript hoặc PHP thường không cần tự tạo boundary thủ công.
Encoding và charset trong giao tiếp HTTP
Encoding là một phần dễ gây nhầm lẫn vì có nhiều lớp mã hóa khác nhau.
Ví dụ, dữ liệu tiếng Việt có thể được biểu diễn bằng UTF-8. Server cần biết cách giải mã byte thành ký tự đúng.
Content-Type: application/json; charset=UTF-8
Trong thực tế hiện đại, JSON thường được sử dụng với UTF-8. Điều quan trọng hơn việc ghi thêm mọi tham số vào header là toàn bộ hệ thống phải thống nhất cách mã hóa dữ liệu.
Đặc biệt, cần phân biệt character encoding với việc mã hóa URL hoặc mã hóa dữ liệu bảo mật. URL encoding, Base64 và mã hóa mật mã là những khái niệm hoàn toàn khác nhau.
Query parameter, path parameter và dữ liệu trong body
API thường nhận dữ liệu từ ba vị trí chính: path, query string và body. Chọn đúng vị trí giúp API dễ hiểu và dễ sử dụng hơn.
Path parameter
Path parameter thường dùng để xác định tài nguyên cụ thể.
GET /api/products/25
Trong trường hợp này, 25 có thể là ID sản phẩm.
Cách thiết kế này dễ đọc vì chỉ nhìn URL đã có thể hiểu request đang nhắm tới tài nguyên nào.
Query parameter
Query parameter phù hợp với các điều kiện truy vấn.
GET /api/products?category=office&page=2&limit=20
Ở đây:
- category dùng để lọc.
- page xác định trang.
- limit xác định số lượng kết quả mong muốn.
Query string đặc biệt phù hợp với tìm kiếm, lọc, sắp xếp và phân trang.
Request body
Body phù hợp với dữ liệu nghiệp vụ có cấu trúc lớn hoặc dữ liệu được gửi để tạo và cập nhật tài nguyên.
POST /api/products
Content-Type: application/json
{
"name": "Bàn làm việc",
"price": 2500000,
"category_id": 4
}
Không nên đưa toàn bộ dữ liệu nghiệp vụ vào query string chỉ vì nó dễ nhìn thấy trên URL. Ngoài vấn đề thiết kế, URL còn có thể xuất hiện trong lịch sử trình duyệt, log máy chủ hoặc các hệ thống trung gian.
Status code và cách đọc kết quả từ HTTP
Status code là một trong những cơ chế quan trọng nhất để server truyền trạng thái xử lý cho client.
Các mã HTTP được chia thành năm nhóm:
| Nhóm | Khoảng mã | Ý nghĩa tổng quát |
|---|---|---|
| 1xx | 100–199 | Thông tin |
| 2xx | 200–299 | Thành công |
| 3xx | 300–399 | Chuyển hướng hoặc điều kiện liên quan đến tài nguyên |
| 4xx | 400–499 | Lỗi từ request hoặc phía client |
| 5xx | 500–599 | Lỗi phía server |
Không cần ghi nhớ mọi status code tồn tại. Quan trọng là hiểu nhóm mã và những mã thường xuất hiện trong API.
200 OK
200 cho biết request đã được xử lý thành công và server trả về kết quả.
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": {
"id": 25,
"name": "Bàn làm việc"
}
}
200 phù hợp với nhiều thao tác đọc dữ liệu và một số thao tác cập nhật có response body.
201 Created
201 phù hợp khi request đã tạo thành công một tài nguyên mới.
HTTP/1.1 201 Created
Ví dụ tạo sản phẩm thành công có thể trả về tài nguyên vừa được tạo:
{
"id": 26,
"name": "Ghế văn phòng",
"price": 1800000
}
204 No Content
204 cho biết request được xử lý thành công nhưng response không có nội dung body.
HTTP/1.1 204 No Content
Đây là lựa chọn phù hợp cho một số thao tác DELETE hoặc những request mà client không cần nhận thêm dữ liệu.
400 Bad Request
400 thường được sử dụng khi request không hợp lệ và server không thể xử lý theo yêu cầu đó.
Ví dụ JSON bị lỗi cú pháp:
{
"name": "Sản phẩm A",
"price":
Hoặc request không đáp ứng định dạng mà API yêu cầu.
401 Unauthorized
401 thường liên quan đến việc request chưa cung cấp thông tin xác thực hợp lệ.
Ví dụ client gọi endpoint yêu cầu đăng nhập nhưng không gửi token.
GET /api/account
Authorization: Bearer invalid-token
Cần lưu ý tên gọi của 401 dễ gây hiểu nhầm. Trong API, nó thường liên quan đến trạng thái xác thực chứ không đơn giản là “không có quyền”.
403 Forbidden
403 phù hợp khi server hiểu request và danh tính của client nhưng từ chối cho phép thực hiện thao tác.
Ví dụ người dùng đã đăng nhập nhưng không có quyền xóa sản phẩm.
DELETE /api/products/25
Trong tình huống này, việc gửi lại cùng thông tin xác thực không nhất thiết giải quyết được vấn đề vì nguyên nhân nằm ở quyền truy cập.
404 Not Found
404 thường được sử dụng khi tài nguyên được yêu cầu không tồn tại hoặc endpoint tương ứng không được tìm thấy.
GET /api/products/999999
Nếu ID 999999 không tồn tại, API có thể trả:
HTTP/1.1 404 Not Found
{
"message": "Không tìm thấy sản phẩm"
}
405 Method Not Allowed
405 xuất hiện khi endpoint tồn tại nhưng method được sử dụng không được phép.
Ví dụ endpoint chỉ hỗ trợ GET nhưng client gửi DELETE.
DELETE /api/products
Trong trường hợp này, vấn đề không nhất thiết là URL sai. Có thể URL đúng nhưng method không phù hợp.
409 Conflict
409 dùng cho những tình huống request tạo ra xung đột với trạng thái hiện tại của tài nguyên.
Ví dụ hệ thống yêu cầu username duy nhất nhưng client cố tạo một tài khoản có username đã tồn tại.
POST /api/users
{
"username": "admin"
}
Server có thể phản hồi:
HTTP/1.1 409 Conflict
{
"message": "Tên người dùng đã tồn tại"
}
422 Unprocessable Content
422 thường được dùng khi request có cấu trúc có thể đọc được nhưng dữ liệu không vượt qua kiểm tra nghiệp vụ hoặc validation.
Ví dụ:
POST /api/users
{
"email": "abc",
"age": -5
}
JSON có thể hoàn toàn hợp lệ về mặt cú pháp nhưng dữ liệu lại không hợp lệ đối với quy tắc của ứng dụng.
Đây là lý do cần phân biệt lỗi cú pháp request với lỗi validation dữ liệu.
429 Too Many Requests
429 cho biết client đã gửi quá nhiều request trong một khoảng thời gian theo chính sách của server.
Mã này thường liên quan đến rate limiting, đặc biệt quan trọng đối với API công khai hoặc các endpoint nhạy cảm.
HTTP/1.1 429 Too Many Requests
{
"message": "Bạn đã gửi quá nhiều yêu cầu"
}
500 Internal Server Error
500 cho biết server gặp lỗi trong quá trình xử lý request.
Ví dụ có thể là exception không được xử lý, lỗi logic hoặc một sự cố nội bộ khác.
API production không nên trả stack trace, thông tin đường dẫn file, câu SQL hoặc thông tin cấu hình nhạy cảm cho client.
502, 503 và 504
Nhóm lỗi này thường xuất hiện rõ hơn khi hệ thống có nhiều lớp hoặc nhiều dịch vụ.
- 502 Bad Gateway: một gateway hoặc proxy nhận response không hợp lệ từ upstream.
- 503 Service Unavailable: server hiện không thể phục vụ request, thường liên quan đến quá tải hoặc bảo trì.
- 504 Gateway Timeout: gateway hoặc proxy không nhận được response kịp thời từ upstream.
Khi API được triển khai phía sau Nginx, load balancer, reverse proxy hoặc nhiều service, hiểu các status code này sẽ giúp việc tìm nguyên nhân lỗi nhanh hơn rất nhiều.
Không nên dùng 200 cho mọi trường hợp
Một lỗi thiết kế API phổ biến là server luôn trả HTTP 200 rồi đưa trạng thái thật vào JSON.
HTTP/1.1 200 OK
{
"success": false,
"error": "Không có quyền truy cập"
}
Cách làm này vẫn có thể hoạt động, nhưng khiến tầng HTTP mất đi ý nghĩa vốn có. Client phải đọc body mới biết request thành công hay thất bại.
Cách rõ ràng hơn:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"success": false,
"error": "Không có quyền truy cập"
}
Ở đây status code mô tả trạng thái HTTP, còn body cung cấp thông tin chi tiết cho ứng dụng.
Hai tầng này bổ trợ cho nhau thay vì cạnh tranh với nhau.
Thiết kế response lỗi có cấu trúc
Một API chuyên nghiệp không chỉ cần status code đúng mà còn cần cách trả lỗi nhất quán.
Ví dụ:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
{
"success": false,
"message": "Dữ liệu không hợp lệ",
"errors": {
"email": [
"Email không đúng định dạng"
],
"password": [
"Mật khẩu phải có ít nhất 8 ký tự"
]
}
}
Cấu trúc này giúp frontend biết lỗi nằm ở trường nào thay vì phải phân tích một đoạn thông báo khó đoán.
Điều quan trọng là format lỗi phải ổn định. Nếu endpoint này trả lỗi theo một cấu trúc, endpoint khác lại sử dụng cấu trúc hoàn toàn khác, việc xử lý phía client sẽ trở nên phức tạp.
Header và status code phải được thiết kế cùng nhau
Trong một response API hoàn chỉnh, status code, header và body không nên được xem là ba phần tách biệt.
Ví dụ một response JSON thành công có thể có:
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": {
"id": 25
}
}
Trong đó:
- Status code cho biết request thành công.
- Content-Type cho biết body là JSON.
- Body chứa dữ liệu nghiệp vụ.
Khi ba lớp này nhất quán, client có thể xử lý API dựa trên quy tắc rõ ràng thay vì phải đoán ý server.
HTTP API trong thực tế với PHP và JavaScript
Với một website PHP, API có thể được xây dựng ngay trên máy chủ hiện tại. JavaScript ở phía trình duyệt gọi endpoint bằng Fetch API hoặc AJAX, PHP tiếp nhận request, xử lý dữ liệu và trả JSON.
Ví dụ JavaScript gửi request:
fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
name: 'Bàn làm việc',
price: 2500000
})
})
.then(response => response.json())
.then(data => {
console.log(data);
});
Phía PHP có thể đọc body:
<?php
header('Content-Type: application/json; charset=UTF-8');
$body = file_get_contents('php://input');
$data = json_decode($body, true);
echo json_encode([
'success' => true,
'data' => $data
]);
?>
Luồng xử lý lúc này khá rõ:
- JavaScript tạo HTTP request.
- Header thông báo body là JSON.
- PHP đọc raw request body.
- PHP giải mã JSON.
- PHP xử lý nghiệp vụ.
- PHP tạo JSON response.
- JavaScript nhận và xử lý response.
Đây chính là nền tảng của rất nhiều chức năng website hiện đại như tìm kiếm không tải lại trang, đăng nhập bằng AJAX, giỏ hàng, quản lý sản phẩm, gửi biểu mẫu và các hệ thống quản trị.
Những lỗi HTTP API thường gặp ở giai đoạn đầu
- Gửi JSON nhưng không khai báo Content-Type phù hợp.
- Đọc JSON bằng $_POST thay vì đọc raw request body.
- Dùng 200 cho cả thành công và lỗi.
- Không phân biệt 401 với 403.
- Dùng query string để truyền một lượng lớn dữ liệu nghiệp vụ.
- Không xác định rõ endpoint nào nhận method nào.
- Trả lỗi không có cấu trúc thống nhất.
- Đưa thông tin lỗi nội bộ của PHP ra response production.
- Không xử lý trường hợp client gửi dữ liệu sai định dạng.
- Không giới hạn số lượng request đối với những endpoint nhạy cảm.
Những lỗi này thường không khiến API ngừng hoạt động ngay lập tức. Vấn đề là chúng tạo ra một hệ thống khó mở rộng, khó debug và dễ phát sinh lỗi khi có nhiều client cùng sử dụng.
Cookie hoạt động như thế nào trong giao tiếp HTTP?
HTTP về bản chất là stateless, nghĩa là mỗi request không tự mang theo toàn bộ lịch sử tương tác trước đó. Server nhận một request mới và không mặc nhiên biết request đó thuộc về người dùng nào nếu ứng dụng không cung cấp cơ chế nhận diện.
Cookie là một trong những cơ chế phổ biến được sử dụng để duy trì thông tin giữa nhiều request.
Server có thể gửi header:
Set-Cookie: session_id=abc123
Trình duyệt lưu cookie theo các quy tắc của nó. Ở những request phù hợp sau đó, trình duyệt có thể gửi lại:
Cookie: session_id=abc123
Nhờ đó server có thể liên kết nhiều request với cùng một phiên làm việc.
Cookie không phải là session
Hai khái niệm này thường bị sử dụng lẫn lộn.
Cookie là dữ liệu được lưu ở phía client và được gửi lại trong những request phù hợp.
Session là cơ chế lưu trạng thái phía server, trong đó cookie thường chỉ chứa một mã nhận diện để server tìm dữ liệu session tương ứng.
Ví dụ:
Set-Cookie: PHPSESSID=abc123
Giá trị abc123 có thể chỉ là mã nhận diện. Dữ liệu thực tế của session có thể nằm trên server.
HttpOnly
Cookie có thể được đánh dấu HttpOnly:
Set-Cookie: session_id=abc123; HttpOnly
Thuộc tính này ngăn JavaScript phía trình duyệt đọc cookie thông qua các API như document.cookie. Nó đặc biệt hữu ích đối với cookie chứa thông tin phiên hoặc thông tin xác thực.
HttpOnly không biến cookie thành tuyệt đối an toàn. Nếu ứng dụng có lỗ hổng XSS, kẻ tấn công vẫn có thể lợi dụng ngữ cảnh trình duyệt của nạn nhân để thực hiện các hành động trái phép, dù không đọc được giá trị cookie.
Secure
Cookie xác thực nên được sử dụng cùng Secure khi hệ thống chạy HTTPS:
Set-Cookie: session_id=abc123; Secure; HttpOnly
Secure yêu cầu cookie chỉ được gửi qua kết nối HTTPS.
SameSite
SameSite kiểm soát việc cookie có được gửi trong những tình huống liên quan đến ngữ cảnh cross-site hay không.
Ví dụ:
Set-Cookie: session_id=abc123; Secure; HttpOnly; SameSite=Lax
Ba mức thường gặp là Strict, Lax và None. Việc lựa chọn phụ thuộc vào kiến trúc website và cách frontend, backend tương tác.
Đối với API sử dụng cookie để xác thực, cần hiểu SameSite thay vì chỉ thêm thuộc tính theo mẫu có sẵn. Một cấu hình quá chặt có thể khiến một số luồng hợp lệ không gửi cookie, trong khi cấu hình quá rộng có thể làm tăng rủi ro liên quan đến cross-site request.
Session trong API
Session đặc biệt quen thuộc với PHP và các website sử dụng cơ chế đăng nhập dựa trên cookie.
Một luồng đơn giản có thể diễn ra như sau:
- Người dùng gửi thông tin đăng nhập.
- Server kiểm tra tài khoản.
- Server tạo session.
- Server gửi cookie chứa session identifier.
- Trình duyệt lưu cookie.
- Các request tiếp theo gửi cookie.
- Server dùng session identifier để xác định trạng thái đăng nhập.
Trong PHP, session có thể được khởi tạo:
<?php
session_start();
$_SESSION['user_id'] = 25;
?>
Ở request tiếp theo:
<?php
session_start();
$userId = $_SESSION['user_id'] ?? null;
?>
Đối với API, session-based authentication vẫn có thể là lựa chọn phù hợp, đặc biệt khi API phục vụ chính website của cùng hệ thống.
Tuy nhiên, khi API được sử dụng bởi ứng dụng di động, nhiều frontend độc lập hoặc các hệ thống bên thứ ba, cách quản lý authentication có thể cần thiết kế khác.
Authentication và Authorization không giống nhau
Đây là một trong những cặp khái niệm quan trọng nhất khi xây dựng API.
Authentication trả lời câu hỏi: “Bạn là ai?”
Authorization trả lời câu hỏi: “Bạn được phép làm gì?”
Ví dụ một người dùng đăng nhập thành công nghĩa là authentication đã được xác nhận. Nhưng điều đó không có nghĩa người dùng được phép xóa mọi sản phẩm.
Một API có thể thực hiện quy trình:
- Nhận thông tin xác thực.
- Xác định danh tính người dùng.
- Kiểm tra trạng thái tài khoản.
- Kiểm tra quyền đối với tài nguyên.
- Cho phép hoặc từ chối thao tác.
Bearer token
Một cách phổ biến để truyền access token là Authorization header:
Authorization: Bearer eyJhbGciOi...
Server nhận token, xác minh nó và xác định danh tính hoặc quyền tương ứng.
Không nên coi việc token có tồn tại trong header là bằng chứng token hợp lệ. Server phải kiểm tra token theo cơ chế xác thực của hệ thống.
API key
Một số API sử dụng API key để nhận diện client hoặc ứng dụng.
X-API-Key: your-api-key
API key phù hợp với một số mô hình machine-to-machine hoặc API dành cho đối tác. Tuy nhiên, API key không nên mặc nhiên được xem là giải pháp thay thế cho mọi hình thức xác thực người dùng.
Khả năng của một credential cần được giới hạn theo đúng mục đích sử dụng. Một key chỉ cần quyền đọc dữ liệu không nên được cấp quyền quản trị toàn hệ thống.
HTTPS bảo vệ API như thế nào?
HTTP truyền dữ liệu giữa client và server. HTTPS bổ sung lớp bảo mật bằng cách sử dụng TLS để bảo vệ kết nối.
Với API có thông tin đăng nhập, token, dữ liệu cá nhân hoặc dữ liệu nghiệp vụ, HTTPS gần như là yêu cầu nền tảng trong môi trường production.
HTTPS giúp bảo vệ:
- Tính bí mật của dữ liệu truyền trên đường truyền.
- Tính toàn vẹn của dữ liệu.
- Việc xác thực server thông qua chứng chỉ số.
Ví dụ thay vì:
http://example.com/api/login
hệ thống production nên sử dụng:
https://example.com/api/login
Điều quan trọng là HTTPS bảo vệ đường truyền. Nó không tự động sửa lỗi SQL Injection, XSS, phân quyền sai, validation thiếu hoặc logic nghiệp vụ không an toàn.
CORS và bài toán frontend gọi API khác origin
CORS là một phần HTTP mà frontend developer rất dễ gặp khi JavaScript gọi API.
Ví dụ website frontend chạy tại:
https://frontend.example.com
nhưng API nằm tại:
https://api.example.com
Hai origin này khác nhau. Trình duyệt sẽ áp dụng chính sách same-origin và CORS để kiểm soát việc JavaScript có được phép đọc response hay không.
Origin gồm những gì?
Origin được xác định bởi ba thành phần chính:
- Scheme.
- Host.
- Port.
Ví dụ:
https://example.com:443
Nếu một trong những thành phần này khác, origin có thể trở thành khác nhau.
Access-Control-Allow-Origin
Server có thể phản hồi:
Access-Control-Allow-Origin: https://frontend.example.com
Header này cho phép origin được chỉ định truy cập response theo chính sách CORS.
Không nên tùy tiện dùng:
Access-Control-Allow-Origin: *
cho mọi API chỉ vì muốn nhanh chóng giải quyết lỗi CORS. Chính sách phù hợp phải dựa trên loại API và yêu cầu bảo mật.
Preflight request
Không phải mọi cross-origin request đều được trình duyệt gửi thẳng đến API theo cách đơn giản. Một số request có thể khiến trình duyệt thực hiện một request kiểm tra trước gọi là preflight.
Preflight thường sử dụng method OPTIONS.
OPTIONS /api/products HTTP/1.1
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Server cần phản hồi chính sách phù hợp, chẳng hạn:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Nếu server không xử lý đúng OPTIONS hoặc không trả các header cần thiết, frontend có thể báo lỗi CORS dù endpoint POST thực tế vẫn tồn tại.
Credentials và cookie khi dùng CORS
Khi API sử dụng cookie, CORS trở nên phức tạp hơn vì trình duyệt cần kiểm soát việc gửi credential.
Phía JavaScript có thể yêu cầu gửi credential:
fetch('https://api.example.com/account', {
credentials: 'include'
});
Server cần cấu hình CORS tương ứng:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true
Khi sử dụng credentials, không thể tùy tiện kết hợp với wildcard origin theo cách cấu hình CORS thông thường.
Đây là lý do lỗi “CORS” đôi khi không nằm ở frontend hoặc backend riêng lẻ. Cookie, SameSite, origin, credentials và response header đều có thể liên quan.
Cache trong HTTP API
Cache giúp giảm số lần phải xử lý request và truyền lại dữ liệu. Với API có lượng truy cập lớn, caching đúng cách có thể cải thiện đáng kể hiệu năng.
HTTP cung cấp nhiều cơ chế để client và các cache trung gian biết response có thể được lưu hay không.
Cache-Control
Server có thể trả:
Cache-Control: max-age=300
Điều này cho phép cache sử dụng response trong khoảng thời gian được chỉ định trước khi cần kiểm tra lại.
Đối với dữ liệu nhạy cảm, cần đặc biệt thận trọng với caching. Không nên để response chứa thông tin riêng tư bị lưu ở một cache không phù hợp.
no-cache và no-store
Hai chỉ thị này không hoàn toàn giống nhau.
no-cache không đơn giản có nghĩa là “không được lưu”. Nó liên quan đến việc cache phải kiểm tra lại với server trước khi sử dụng response trong những trường hợp phù hợp.
no-store yêu cầu không lưu response theo cơ chế cache.
Ví dụ:
Cache-Control: no-store
thường phù hợp hơn với những response chứa dữ liệu nhạy cảm mà ứng dụng không muốn được cache.
ETag và kiểm tra tài nguyên đã thay đổi hay chưa
ETag là một cơ chế giúp server cung cấp định danh cho một phiên bản biểu diễn của tài nguyên.
Ví dụ response:
HTTP/1.1 200 OK
ETag: "product-25-v7"
Content-Type: application/json
Ở request sau, client hoặc cache có thể gửi:
If-None-Match: "product-25-v7"
Nếu tài nguyên chưa thay đổi, server có thể trả:
HTTP/1.1 304 Not Modified
Khi đó không cần gửi lại toàn bộ response body.
Cơ chế này có thể giúp giảm lượng dữ liệu truyền và tăng hiệu quả sử dụng cache, đặc biệt với tài nguyên được truy cập thường xuyên nhưng ít thay đổi.
Conditional request trong API
Conditional request cho phép client đặt điều kiện trước khi server thực hiện hoặc trả dữ liệu.
Một số header thường gặp gồm:
- If-None-Match.
- If-Match.
- If-Modified-Since.
- If-Unmodified-Since.
Điều này không chỉ hữu ích cho cache. Nó còn có thể hỗ trợ kiểm soát xung đột khi nhiều client cùng thao tác trên một tài nguyên.
Ví dụ một client đang chỉnh sửa sản phẩm. Trong lúc đó một client khác đã thay đổi sản phẩm. Nếu API chỉ cập nhật dựa trên ID mà không kiểm tra phiên bản, dữ liệu của client sau có thể ghi đè thay đổi trước đó.
Với ETag và If-Match, server có thể yêu cầu client chỉ cập nhật nếu tài nguyên vẫn đúng phiên bản mà client đã đọc.
If-Match: "product-25-v7"
Nếu phiên bản đã thay đổi, server có thể từ chối request thay vì âm thầm ghi đè dữ liệu mới.
Rate limiting và bảo vệ endpoint
API công khai hoặc API có endpoint nhạy cảm cần có cơ chế giới hạn tần suất request.
Mục đích của rate limiting không chỉ là chống tấn công. Nó còn giúp ngăn client bị lỗi logic và gửi hàng nghìn request ngoài ý muốn.
Khi vượt giới hạn, API có thể trả:
HTTP/1.1 429 Too Many Requests
Server có thể cung cấp thêm thông tin để client biết khi nào nên thử lại.
Retry-After: 60
Giá trị này có thể được sử dụng để thông báo khoảng thời gian client nên chờ trước khi thực hiện request tiếp theo.
Validation dữ liệu phải thực hiện ở phía server
Frontend có thể kiểm tra dữ liệu để cải thiện trải nghiệm người dùng, nhưng không được xem validation phía frontend là lớp bảo vệ cuối cùng.
Client hoàn toàn có thể bỏ qua JavaScript và gửi request trực tiếp:
POST /api/users
Content-Type: application/json
{
"email": "attacker@example.com",
"role": "admin"
}
Server phải tự kiểm tra:
- Kiểu dữ liệu.
- Độ dài.
- Định dạng.
- Giá trị cho phép.
- Quan hệ giữa các trường.
- Quyền của người gửi request.
Không nên tin bất kỳ dữ liệu nào chỉ vì nó được gửi từ giao diện website chính thức.
HTTP API và các rủi ro bảo mật thường gặp
HTTP chỉ là lớp giao tiếp. Một API an toàn cần kết hợp HTTP với các biện pháp bảo vệ ở tầng ứng dụng.
SQL Injection
Dữ liệu nhận từ request không được nối trực tiếp vào câu SQL.
Ví dụ nguy hiểm:
<?php
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
?>
Thay vào đó nên sử dụng prepared statement hoặc cơ chế truy vấn an toàn do thư viện cơ sở dữ liệu cung cấp.
XSS
Nếu API nhận dữ liệu từ người dùng rồi frontend đưa dữ liệu đó trực tiếp vào HTML mà không xử lý phù hợp, XSS có thể xảy ra.
API trả JSON không có nghĩa dữ liệu bên trong JSON tự động an toàn. An toàn còn phụ thuộc vào cách client sử dụng dữ liệu.
CSRF
CSRF đặc biệt đáng chú ý với hệ thống xác thực dựa trên cookie vì trình duyệt có thể tự động gửi cookie trong những request phù hợp.
Việc bảo vệ CSRF cần được thiết kế dựa trên cách authentication được triển khai, SameSite cookie và cơ chế kiểm tra request của ứng dụng.
Mass assignment
Một lỗi nguy hiểm khác là server lấy toàn bộ dữ liệu JSON rồi cập nhật trực tiếp vào model mà không giới hạn trường được phép thay đổi.
Ví dụ client gửi:
{
"name": "Người dùng A",
"email": "a@example.com",
"is_admin": true
}
Nếu endpoint chỉ cho phép người dùng sửa tên và email nhưng server vô tình cập nhật luôn is_admin, đây là lỗi phân quyền nghiêm trọng.
API cần xác định rõ trường nào client được phép gửi và trường nào chỉ server mới được thay đổi.
Không đưa thông tin nhạy cảm vào URL
URL thường xuất hiện trong nhiều hệ thống trung gian như access log, lịch sử trình duyệt, công cụ phân tích hoặc monitoring.
Vì vậy không nên truyền password, access token hoặc dữ liệu bí mật trong query string:
GET /api/login?email=user@example.com&password=secret123
Cách này vừa không phù hợp với ý nghĩa của GET đối với thao tác đăng nhập, vừa làm tăng nguy cơ dữ liệu nhạy cảm xuất hiện trong log.
Thông tin xác thực nên được truyền qua cơ chế phù hợp như request body hoặc Authorization header tùy thiết kế authentication.
HTTP và bảo mật phải được nhìn như một chuỗi
Một API không thể trở nên an toàn chỉ bằng cách thêm HTTPS.
Cần nhìn toàn bộ chuỗi:
Transport → HTTP → Authentication → Authorization → Validation → Business Logic → Database → Response
Nếu HTTPS được cấu hình tốt nhưng API cho phép người dùng sửa ID của người khác, hệ thống vẫn có lỗ hổng.
Nếu authentication tốt nhưng server trả stack trace chứa thông tin nội bộ, API vẫn có vấn đề.
Nếu CORS được cấu hình đúng nhưng token được lưu và quản lý không an toàn, hệ thống vẫn có rủi ro.
Vì vậy hiểu HTTP phải đi cùng tư duy về toàn bộ vòng đời request.
Checklist HTTP quan trọng khi xây dựng API
- Xác định rõ method của từng endpoint.
- Phân biệt path parameter, query parameter và request body.
- Khai báo Content-Type chính xác.
- Sử dụng Accept khi cần thương lượng định dạng response.
- Dùng status code phù hợp với kết quả xử lý.
- Thiết kế format response lỗi thống nhất.
- Không đưa dữ liệu nhạy cảm vào URL.
- Luôn kiểm tra dữ liệu ở phía server.
- Phân biệt authentication và authorization.
- Sử dụng HTTPS trong môi trường production.
- Cấu hình cookie với các thuộc tính phù hợp.
- Hiểu CORS thay vì xử lý bằng cách mở quyền tùy tiện.
- Kiểm soát cache đối với dữ liệu nhạy cảm.
- Cân nhắc rate limiting cho endpoint phù hợp.
- Không trả thông tin lỗi nội bộ ra bên ngoài.
Đến đây đã có thể nhìn HTTP API dưới góc độ đầy đủ hơn: request không chỉ là URL, response không chỉ là JSON và bảo mật không chỉ là HTTPS. Tất cả các lớp này kết hợp với nhau để tạo nên một API có hành vi nhất quán.
Từ HTTP đến cách thiết kế API có cấu trúc
Hiểu HTTP mới chỉ là nền móng. Khi bắt đầu xây dựng API, vấn đề tiếp theo là biến những quy tắc của HTTP thành một giao diện dễ sử dụng, dễ mở rộng và ít gây hiểu nhầm cho client.
Một API tốt cần trả lời được những câu hỏi cơ bản:
- Tài nguyên nào đang được cung cấp?
- Client dùng method nào để thao tác?
- Dữ liệu đầu vào nằm ở đâu?
- Response có cấu trúc thế nào?
- Lỗi được biểu diễn ra sao?
- Authentication và authorization được xử lý thế nào?
- Client có thể retry request hay không?
- API có thay đổi mà không phá vỡ client cũ hay không?
Đây là lý do thiết kế API không nên bắt đầu bằng việc nghĩ ra thật nhiều URL. Nên bắt đầu từ tài nguyên và hành vi mà hệ thống cần cung cấp.
Resource và tư duy REST khi xây dựng endpoint
REST là một phong cách kiến trúc thường được sử dụng để thiết kế hệ thống dựa trên HTTP. Không phải API nào dùng HTTP cũng là RESTful hoàn toàn, nhưng các nguyên tắc REST có thể giúp cấu trúc API rõ ràng hơn.
Ví dụ thay vì đặt tên endpoint theo hành động:
GET /api/getProducts
POST /api/createProduct
POST /api/deleteProduct
có thể biểu diễn tài nguyên:
GET /api/products
POST /api/products
DELETE /api/products/25
Method thể hiện loại thao tác, còn URL tập trung vào tài nguyên.
Cách này giúp API có quy luật dễ dự đoán hơn. Khi client đã hiểu cách làm việc với một resource, họ có thể áp dụng tư duy tương tự cho các resource khác.
CRUD và HTTP
CRUD gồm Create, Read, Update và Delete. Đây là mô hình rất phổ biến khi quản lý dữ liệu.
| Thao tác | Method thường dùng | Ví dụ |
|---|---|---|
| Create | POST | /api/products |
| Read danh sách | GET | /api/products |
| Read một bản ghi | GET | /api/products/25 |
| Update | PUT hoặc PATCH | /api/products/25 |
| Delete | DELETE | /api/products/25 |
Đây không phải quy tắc bắt buộc cho mọi API, nhưng là một quy ước dễ hiểu và tận dụng tốt ý nghĩa của HTTP method.
Thiết kế danh sách với phân trang
Không nên trả hàng trăm nghìn bản ghi trong một response chỉ vì client yêu cầu danh sách.
API thường cung cấp cơ chế phân trang:
GET /api/products?page=2&limit=20
Trong đó page xác định trang và limit xác định số lượng kết quả mong muốn.
Server nên đặt giới hạn tối đa cho limit. Nếu client gửi:
GET /api/products?page=1&limit=1000000
thì server không nên truy vấn và trả về một triệu bản ghi chỉ vì client yêu cầu như vậy.
Response phân trang
Một response có thể chứa dữ liệu và thông tin phân trang:
{
"data": [
{
"id": 21,
"name": "Sản phẩm A"
},
{
"id": 22,
"name": "Sản phẩm B"
}
],
"pagination": {
"page": 2,
"limit": 20,
"total": 125,
"total_pages": 7
}
}
Cấu trúc thực tế có thể khác nhau, nhưng nguyên tắc quan trọng là client phải biết mình đang ở đâu và còn bao nhiêu dữ liệu để tiếp tục truy vấn.
Lọc, tìm kiếm và sắp xếp
Query string rất phù hợp với các yêu cầu thay đổi cách lấy danh sách.
GET /api/products?category=office&status=active&sort=price&order=asc
Cách này giúp URL vẫn thể hiện đúng bản chất: client đang lấy danh sách sản phẩm nhưng có thêm các điều kiện truy vấn.
Cần xác định rõ các giá trị được phép. Không nên lấy trực tiếp một tham số sort từ client rồi ghép thẳng vào câu SQL.
Ví dụ client gửi:
?sort=price
Server nên ánh xạ price vào một cột được cho phép thay vì coi giá trị client gửi là tên cột SQL đáng tin cậy.
Timeout quan trọng hơn việc chờ vô hạn
Một API có thể gặp tình trạng server xử lý quá lâu, database chậm hoặc dịch vụ bên ngoài không phản hồi.
Nếu client chờ vô hạn, một request bị treo có thể kéo theo nhiều request khác bị tồn đọng.
Client nên có timeout phù hợp với loại thao tác.
Ví dụ một request lấy dữ liệu đơn giản có thể có timeout ngắn hơn một tác vụ xử lý phức tạp.
Timeout không có nghĩa server đã chắc chắn không xử lý request. Đây là điểm cực kỳ quan trọng khi thiết kế retry.
Retry và nguy cơ tạo dữ liệu trùng
Giả sử client gửi request tạo đơn hàng:
POST /api/orders
Server đã tạo đơn thành công nhưng response bị mất do lỗi mạng. Client không biết thao tác đã thành công hay chưa và gửi lại request.
Nếu server tạo thêm một đơn hàng, người dùng có thể bị tạo đơn trùng.
Đây là lý do retry không thể được thiết kế chỉ ở phía client. Với các thao tác quan trọng, server cần cơ chế giúp nhận diện request lặp.
Idempotency key
Một giải pháp phổ biến là client gửi một khóa duy nhất cho mỗi thao tác:
POST /api/orders
Idempotency-Key: order-request-8f72c1
Server lưu kết quả tương ứng với key trong một khoảng thời gian phù hợp. Nếu cùng key được gửi lại, server có thể nhận biết đây là request lặp thay vì tạo thêm một tài nguyên mới.
Cơ chế này đặc biệt hữu ích với thanh toán, tạo đơn hàng, gửi giao dịch hoặc các thao tác mà việc thực hiện hai lần gây hậu quả nghiêm trọng.
Timeout, retry và idempotency phải được thiết kế cùng nhau
Ba khái niệm này liên quan chặt chẽ:
- Timeout: client không chờ một request vô hạn.
- Retry: client thử lại khi request có khả năng thất bại do vấn đề tạm thời.
- Idempotency: hệ thống kiểm soát tác động của việc request được gửi lại.
Nếu chỉ thêm retry mà không quan tâm idempotency, hệ thống có thể biến một lỗi mạng thành lỗi nghiệp vụ.
Ngược lại, nếu API hỗ trợ retry nhưng client không có chiến lược timeout, request có thể vẫn bị treo quá lâu trước khi thử lại.
Versioning khi API cần phát triển
API hiếm khi đứng yên. Sau một thời gian, cấu trúc dữ liệu hoặc quy tắc nghiệp vụ có thể thay đổi.
Ví dụ phiên bản đầu tiên:
GET /api/v1/products
Phiên bản mới:
GET /api/v2/products
Versioning giúp client cũ tiếp tục sử dụng phiên bản mà nó hiểu trong khi client mới chuyển sang phiên bản mới.
Không phải thay đổi nào cũng cần tạo version mới. Những thay đổi tương thích ngược có thể được triển khai mà không cần phá vỡ client hiện tại.
Điều quan trọng là phải xác định trước thế nào là breaking change.
Breaking change là gì?
Một thay đổi có thể trở thành breaking change nếu client đang hoạt động bình thường nhưng sau khi API cập nhật lại không thể xử lý response hoặc request.
Ví dụ API đang trả:
{
"id": 25,
"name": "Bàn làm việc"
}
Nhưng phiên bản mới xóa hoàn toàn name mà không có cơ chế chuyển tiếp. Client cũ có thể gặp lỗi nếu phụ thuộc vào trường này.
Vì vậy API cần được phát triển với sự quan tâm đến những client đang sử dụng nó, không chỉ với code phía server.
Debug HTTP API bằng công cụ
Khi API lỗi, đọc code server chưa chắc là cách nhanh nhất để tìm nguyên nhân. Trước tiên cần xác định request thực tế được gửi đi là gì và response thực tế nhận được là gì.
DevTools của trình duyệt
Trong trình duyệt, tab Network cho phép quan sát nhiều thành phần của request:
- URL.
- Method.
- Status code.
- Request headers.
- Request payload.
- Response headers.
- Response body.
- Thời gian xử lý.
Ví dụ JavaScript báo lỗi API, thay vì chỉ đọc thông báo lỗi chung chung, hãy kiểm tra request trong Network để biết request thực tế có phải:
POST /api/products
hay không, Content-Type có đúng không, payload có dữ liệu không và server trả status code nào.
curl
curl là công cụ rất hữu ích để kiểm tra API mà không phụ thuộc frontend.
curl -X GET "https://example.com/api/products"
-H "Accept: application/json"
Gửi JSON:
curl -X POST "https://example.com/api/products"
-H "Content-Type: application/json"
-H "Accept: application/json"
-d '{"name":"Bàn làm việc","price":2500000}'
Nếu curl hoạt động nhưng frontend không hoạt động, phạm vi cần kiểm tra có thể thu hẹp đáng kể sang CORS, cookie, credentials hoặc cách JavaScript tạo request.
Đọc thời gian HTTP để tìm điểm nghẽn
Một request chậm không nhất thiết có nghĩa PHP chạy chậm.
Thời gian tổng thể có thể bao gồm:
- DNS lookup.
- Kết nối TCP.
- TLS handshake.
- Thời gian chờ server.
- Thời gian xử lý PHP.
- Truy vấn database.
- Gọi API bên ngoài.
- Truyền response về client.
Vì vậy khi tối ưu API, cần xác định thời gian đang tiêu tốn ở đâu trước khi thay đổi code.
Ví dụ API PHP truy vấn database mất 2 giây thì tối ưu CSS hoặc JavaScript không giải quyết được nguyên nhân chính.
Connection, keep-alive và HTTP hiện đại
HTTP không chỉ liên quan đến nội dung request và response. Cách kết nối được duy trì cũng ảnh hưởng đến hiệu năng.
Với các phiên bản HTTP hiện đại, việc tái sử dụng kết nối giúp giảm chi phí phải thiết lập kết nối mới cho từng request.
HTTP/1.1 đã hỗ trợ persistent connection, trong khi HTTP/2 và HTTP/3 đưa hiệu quả truyền tải lên những mức khác với các cơ chế hiện đại hơn.
Lập trình viên API không nhất thiết phải tự triển khai HTTP/2 hoặc HTTP/3, nhưng nên hiểu rằng hiệu năng API phụ thuộc cả tầng ứng dụng lẫn tầng mạng.
HTTP/1.1, HTTP/2 và HTTP/3 khác nhau ở đâu?
Cả ba đều phục vụ việc trao đổi dữ liệu HTTP nhưng cách vận chuyển và tối ưu kết nối có sự khác biệt.
| Phiên bản | Đặc điểm đáng chú ý | Góc nhìn khi làm API |
|---|---|---|
| HTTP/1.1 | Giao tiếp phổ biến, cấu trúc đơn giản, text-based | Dễ quan sát và tương thích rộng |
| HTTP/2 | Multiplexing, nén header và nhiều tối ưu truyền tải | Giảm chi phí khi có nhiều request |
| HTTP/3 | Sử dụng QUIC trên UDP thay cho TCP | Cải thiện một số đặc tính kết nối và khả năng phục hồi |
Việc lựa chọn phiên bản HTTP thường nằm ở web server, reverse proxy, CDN và hạ tầng triển khai hơn là trong code endpoint PHP.
Điều quan trọng đối với lập trình viên là không nhầm phiên bản HTTP với phiên bản API. HTTP/2 và API v2 là hai khái niệm hoàn toàn khác nhau.
Content negotiation và khả năng hỗ trợ nhiều định dạng
Client có thể thông báo định dạng mà nó ưu tiên thông qua Accept.
Accept: application/json
Trong một số hệ thống, server có thể hỗ trợ nhiều representation khác nhau.
Ví dụ:
Accept: application/json
hoặc:
Accept: application/xml
API hiện đại thường tập trung vào JSON, nhưng hiểu content negotiation giúp lập trình viên hiểu rõ hơn vai trò của Accept và Content-Type.
Accept nói về dữ liệu client muốn nhận. Content-Type nói về dữ liệu hiện đang được truyền trong body.
Location header sau khi tạo tài nguyên
Khi server tạo một tài nguyên mới, response có thể cung cấp địa chỉ của tài nguyên thông qua Location.
HTTP/1.1 201 Created
Location: https://example.com/api/products/26
Content-Type: application/json
Điều này giúp client biết tài nguyên mới nằm ở đâu mà không phải tự suy đoán URL.
Đây là một ví dụ cho thấy HTTP cung cấp nhiều cơ chế hữu ích mà API có thể tận dụng thay vì tự tạo quy ước riêng.
API nên nhất quán hơn là quá phức tạp
Một API tốt không phải API có nhiều header nhất, nhiều loại status code nhất hay cấu trúc JSON phức tạp nhất.
Giá trị lớn nhất nằm ở tính nhất quán.
Nếu endpoint tạo dữ liệu luôn dùng một quy tắc, endpoint cập nhật cũng theo quy tắc đó, lỗi validation có cùng cấu trúc và authentication được xử lý thống nhất, client sẽ dễ phát triển hơn rất nhiều.
Ngược lại, API có thể sử dụng đúng từng status code nhưng mỗi endpoint lại có một cách trả dữ liệu khác nhau thì trải nghiệm tích hợp vẫn rất tệ.
Checklist thiết kế HTTP API trước khi đưa lên production
- Endpoint có tên rõ ràng và phản ánh đúng tài nguyên.
- HTTP method được sử dụng đúng ý nghĩa.
- Path parameter và query parameter được phân biệt rõ.
- Request body có định dạng được xác định bằng Content-Type.
- Response khai báo Content-Type phù hợp.
- Status code phản ánh đúng kết quả xử lý.
- Response thành công có cấu trúc ổn định.
- Response lỗi có cấu trúc ổn định.
- Validation được thực hiện ở server.
- Authentication và authorization được tách biệt.
- Dữ liệu nhạy cảm không xuất hiện trong URL.
- HTTPS được sử dụng trong production.
- Cookie có thuộc tính bảo mật phù hợp nếu được sử dụng.
- CORS chỉ mở cho những origin thực sự cần thiết.
- Endpoint nhạy cảm có rate limiting phù hợp.
- API có timeout và chiến lược retry hợp lý.
- Các thao tác có nguy cơ tạo trùng có cơ chế idempotency phù hợp.
- Cache được cấu hình dựa trên tính chất dữ liệu.
- Dữ liệu pagination có giới hạn hợp lý.
- Không để thông tin lỗi nội bộ xuất hiện trong production response.
- Có logging và monitoring đủ để điều tra lỗi.
- Các breaking change được kiểm soát bằng chiến lược versioning phù hợp.
Tư duy quan trọng nhất khi làm HTTP API
HTTP API không nên được hiểu đơn giản là một địa chỉ để gửi request và nhận JSON. Đó là một giao diện giao tiếp có quy tắc, trong đó từng thành phần đều có vai trò.
Method thể hiện ý định của client. URL xác định tài nguyên. Query string bổ sung điều kiện truy vấn. Header cung cấp metadata và chính sách xử lý. Body mang dữ liệu. Status code mô tả kết quả ở tầng HTTP. Response body cung cấp thông tin mà ứng dụng cần.
Khi hệ thống có authentication, cookie, CORS, cache, retry và nhiều client cùng hoạt động, những thành phần này liên kết thành một chuỗi lớn hơn. Một thay đổi nhỏ ở HTTP có thể ảnh hưởng đến frontend, backend, proxy, CDN hoặc ứng dụng di động.
Với lập trình web bằng PHP và JavaScript, nắm chắc HTTP giúp việc xây dựng AJAX, REST API, hệ thống đăng nhập, upload, tìm kiếm, giỏ hàng và các chức năng tương tác trở nên dễ kiểm soát hơn. Thay vì xử lý lỗi theo kiểu thử và sửa, lập trình viên có thể nhìn vào request và response để xác định chính xác vấn đề nằm ở đâu.
Đó cũng là nền tảng để Web Mới xây dựng những hệ thống website và API có khả năng mở rộng: không chỉ làm cho chức năng chạy được, mà còn thiết kế giao tiếp giữa client và server theo những quy tắc rõ ràng, nhất quán và có thể phát triển lâu dài.
Tóm tắt toàn bộ kiến thức HTTP cần nhớ khi làm API
| Thành phần | Cần nhớ |
|---|---|
| Request | Client gửi yêu cầu đến server |
| Response | Server trả kết quả cho client |
| URL | Xác định địa chỉ tài nguyên hoặc endpoint |
| GET | Thường dùng để đọc dữ liệu |
| POST | Thường dùng để tạo hoặc thực hiện thao tác |
| PUT | Thường dùng để thay thế hoặc cập nhật tài nguyên |
| PATCH | Thường dùng để cập nhật một phần |
| DELETE | Thường dùng để xóa |
| Header | Mô tả request, response và các chính sách liên quan |
| Content-Type | Cho biết định dạng body đang truyền |
| Accept | Cho biết định dạng response client mong muốn |
| Cookie | Cơ chế lưu và gửi dữ liệu phía client theo chính sách trình duyệt |
| Session | Cơ chế duy trì trạng thái phía server |
| Authentication | Xác định danh tính |
| Authorization | Xác định quyền thực hiện hành động |
| CORS | Kiểm soát truy cập tài nguyên giữa các origin trong trình duyệt |
| Cache | Giảm việc tải và xử lý lại dữ liệu |
| ETag | Hỗ trợ kiểm tra phiên bản representation của tài nguyên |
| 4xx | Nhóm lỗi liên quan request hoặc phía client |
| 5xx | Nhóm lỗi phía server hoặc hệ thống trung gian |
| HTTPS | Bảo vệ dữ liệu trên đường truyền bằng TLS |
| Rate limiting | Kiểm soát tần suất request |
| Idempotency | Kiểm soát tác động khi cùng thao tác được gửi lại |
| Versioning | Giúp API phát triển mà hạn chế phá vỡ client cũ |
Hiểu được những thành phần trên là đã có nền tảng rất vững để đọc tài liệu API, thiết kế endpoint, debug request và xây dựng backend HTTP thực tế. Khi làm sâu hơn, mỗi thành phần lại mở ra một nhóm kiến thức riêng về mạng, bảo mật, hiệu năng và kiến trúc hệ thống.
- 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 *