ResponseEntity và HTTP Status Chuẩn Ngữ Nghĩa REST
Hướng dẫn sử dụng ResponseEntity, chọn HTTP status code đúng ngữ nghĩa REST và cơ chế HttpMessageConverter tuần tự hóa JSON trong Spring MVC.
TL;DR: Spring MVC cung cấp 3 cách trả response: trả DTO trực tiếp (mặc định 200 OK), dùng ResponseEntity (toàn quyền kiểm soát status code, headers, body), hoặc gắn @ResponseStatus tĩnh trên method. HTTP status codes không phải con số tùy ý mà là một bản giao kèo kỹ thuật (REST contract). POST tạo mới tài nguyên phải trả về 201 Created kèm header Location, DELETE thành công không trả dữ liệu phải dùng 204 No Content, và tuyệt đối không bao giờ trả 200 OK kèm payload báo lỗi. Ở tầng dưới, Spring MVC sử dụng HttpMessageConverter (tiêu biểu là Jackson) để tuần tự hóa Java object thành luồng byte JSON.
1. Spring MVC cung cấp những cách nào để trả về HTTP response?
Trong controller, lập trình viên có thể linh hoạt lựa chọn giữa 3 hình thức trả dữ liệu tùy thuộc vào yêu cầu nghiệp vụ:
Cách 1: Trả DTO trực tiếp (Mặc định 200 OK)
Phù hợp cho các thao tác đọc dữ liệu đơn giản khi kết quả luôn trả về mã 200:
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
return orderService.findById(id);
// Spring tu dong: HTTP 200 OK, Content-Type: application/json
}
Cách 2: Sử dụng ResponseEntity (Kiểm soát toàn diện)
Được khuyến nghị cho các thao tác cần chỉ định status code cụ thể, thêm HTTP headers, hoặc thay đổi kiểu trả về linh hoạt:
@PostMapping("/orders")
public ResponseEntity<OrderDto> createOrder(@Valid @RequestBody OrderRequest req) {
OrderDto created = orderService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
// HTTP 201 Created + Location header + Body
}
Cách 3: Gắn annotation @ResponseStatus (Khai báo tĩnh)
Thích hợp cho các method không trả dữ liệu (trả về void):
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable Long id) {
orderService.delete(id);
// HTTP 204 No Content, body rong
}
Bảng so sánh 3 cơ chế trả response
| Tiêu chí | Trả DTO trực tiếp | ResponseEntity | @ResponseStatus |
|---|---|---|---|
| Mã trạng thái (Status) | Cố định 200 | Tùy biến linh hoạt | Cố định theo annotation |
| Tùy chỉnh Header | Không | Đầy đủ | Không |
| Body theo điều kiện | Không | Có thể thay đổi | Không có body (thường dùng void) |
| Ngữ cảnh sử dụng | GET đơn giản | POST, Conditional GET, Custom Header | DELETE, tác vụ Async |
2. Các nhóm HTTP status code mang ngữ nghĩa chuẩn REST như thế nào?
Mã trạng thái HTTP (RFC 9110) phân thành các nhóm rõ ràng. Sử dụng sai mã trạng thái sẽ phá vỡ REST contract, buộc phía client phải bóc tách toàn bộ response body mới biết trạng thái thực sự.

Nhóm 2xx: Thao tác thành công
- 200 OK: Yêu cầu đã thành công và có dữ liệu trả về trong body.
- 201 Created: Tài nguyên mới đã được tạo thành công. Luôn đính kèm header
Locationchỉ đến URI của tài nguyên. - 202 Accepted: Yêu cầu đã được tiếp nhận để xử lý bất đồng bộ, chưa có kết quả ngay lập tức (ví dụ export file, tính toán báo cáo).
- 204 No Content: Xử lý thành công nhưng không có dữ liệu trả về (chuẩn cho method DELETE).
Nhóm 3xx: Chuyển hướng và Caching
Thường dùng nhất là 304 Not Modified trong conditional request. Client gửi header If-None-Match, nếu dữ liệu không thay đổi, server trả về 304 không kèm body nhằm tiết kiệm băng thông mạng.
Nhóm 4xx: Lỗi xuất phát từ phía Client
Client gửi dữ liệu không hợp lệ, thiếu quyền hạn, hoặc tài nguyên không tồn tại:
| Mã lỗi | Ý nghĩa | Khi nào áp dụng |
|---|---|---|
| 400 Bad Request | Cú pháp không hợp lệ | Thiếu tham số bắt buộc, sai định dạng JSON, validation thất bại |
| 401 Unauthorized | Chưa xác thực | Không cung cấp token hoặc token không hợp lệ |
| 403 Forbidden | Không có quyền truy cập | Đã đăng nhập nhưng không đủ quyền hạn (Role/Permission) |
| 404 Not Found | Không tìm thấy | ID tài nguyên không tồn tại trong hệ thống |
| 409 Conflict | Xung đột trạng thái | Email đã đăng ký, version xung đột khi concurrent update |
| 422 Unprocessable Entity | Sai ngữ nghĩa nghiệp vụ | Dữ liệu đúng cú pháp nhưng vi phạm logic (ngày kết thúc trước ngày bắt đầu) |
| 429 Too Many Requests | Vượt giới hạn tần suất | Client bị rate-limiting |
Nhóm 5xx: Lỗi xuất phát từ phía Server
Client gửi đúng định dạng nhưng máy chủ gặp sự cố không thể đáp ứng:
- 500 Internal Server Error: Lỗi ngoại lệ chưa được xử lý trong code backend.
- 502 Bad Gateway: Service trung gian hoặc upstream service (Microservice, DB) gặp sự cố.
- 503 Service Unavailable: Máy chủ quá tải hoặc đang trong trạng thái bảo trì.
- 504 Gateway Timeout: Upstream service không phản hồi trong thời gian quy định.
3. ResponseEntity builder pattern được sử dụng linh hoạt ra sao?
ResponseEntity cung cấp Fluent Builder API giúp xây dựng response rõ ràng và an toàn kiểu dữ liệu:
// Static factory methods pho bien
ResponseEntity.ok(body); // 200 kem body
ResponseEntity.created(uri).body(createdDto); // 201 kem Location header va body
ResponseEntity.accepted().build(); // 202 khong body
ResponseEntity.noContent().build(); // 204 khong body
ResponseEntity.notFound().build(); // 404
ResponseEntity.status(HttpStatus.CONFLICT).body(errorDetail); // Tuy bien status
// Tuy bien headers day du
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(totalElements))
.header("X-Request-Id", correlationId)
.eTag("\"" + entity.getVersion() + "\"")
.body(entityList);
Xử lý kết quả tìm kiếm có điều kiện với Optional
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) {
return orderService.findOptional(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
4. HttpMessageConverter tuần tự hóa đối tượng Java thành JSON như thế nào?
Khi một controller method trả về đối tượng Java, luồng xử lý diễn ra qua 5 bước tuần tự:
DispatcherServletủy quyền choHandlerMethodReturnValueHandlerxử lý giá trị trả về.- Handler kiểm tra header
Accepttừ phía request client (ví dụapplication/json). - Spring quét qua danh sách
HttpMessageConverterđã đăng ký để tìm converter phù hợp với kiểu dữ liệu Java và media type. MappingJackson2HttpMessageConverterđược chọn, kích hoạtObjectMapper.writeValue()để chuyển đối tượng Java thành chuỗi byte JSON.- Spring ghi chuỗi bytes vào
HttpServletResponse.getOutputStream()và thiết lập headerContent-Type: application/json.
Nếu client yêu cầu một định dạng không được hỗ trợ (ví dụ Accept: application/xml nhưng hệ thống không tích hợp XML library), Spring MVC sẽ ném ra lỗi 406 Not Acceptable.
5. Những HTTP headers quan trọng nào thường dùng trong REST response?
| Header | Vai trò | Ví dụ thực tế |
|---|---|---|
Location | Đường dẫn URI đến tài nguyên mới tạo | /api/v1/orders/1024 |
ETag | Mã hash xác thực phiên bản dữ liệu phục vụ caching | "a8f5c3b2" |
Cache-Control | Chỉ thị caching cho Browser và CDN | public, max-age=3600 |
Content-Disposition | Chỉ định tải tệp đính kèm | attachment; filename="report.xlsx" |
X-Total-Count | Tổng số bản ghi hỗ trợ phân trang | 150 |
Retry-After | Thời gian chờ (giây) trước khi client được phép gửi lại request | 120 |
@GetMapping("/reports/{id}/download")
public ResponseEntity<byte[]> downloadReport(@PathVariable Long id) {
byte[] data = reportService.exportPdf(id);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(data);
}
6. Những sai lầm phổ biến khi thiết kế response status là gì?
Lỗi 1: POST tạo mới nhưng trả về 200 OK không kèm Location
Client không thể biết được URI của tài nguyên mới mà phải tự suy đoán URL hoặc bóc tách body:
// Anti-pattern: Thieu ma trang thai 201 va Location header
@PostMapping("/products")
public ProductDto createProduct(@Valid @RequestBody ProductRequest req) {
return productService.create(req);
}
Lỗi 2: Trả về 200 OK kèm mã lỗi trong body
Đây là lỗi vi phạm nghiêm trọng nguyên lý RESTful:
// Anti-pattern: Tra ve 200 cho resource khong ton tai
@GetMapping("/users/{id}")
public Map<String, Object> getUser(@PathVariable Long id) {
if (!userService.exists(id)) {
return Map.of("error", "User not found"); // Tra ve HTTP 200 voi noi dung loi!
}
return Map.of("data", userService.get(id));
}

Hậu quả: Axios hoặc Fetch API phía frontend không bắt được lỗi qua .catch(), proxy/CDN có thể cache nhầm response lỗi, và hệ thống APM (Datadog, New Relic) ghi nhận 100% request thành công khiến kỹ sư không nhận được cảnh báo.
7. ResponseEntity liên kết với các thành phần khác trong ứng dụng ra sao?
- ArgumentResolver: Xử lý đầu vào (Request). Hai cơ chế ArgumentResolver và ReturnValueHandler kết hợp tạo nên chu trình xử lý đối xứng khép kín trong
DispatcherServlet. - Streaming & HATEOAS: Mở rộng tính năng trả về response nâng cao với Server-Sent Events, Streaming response, và ResponseBodyAdvice.
- RFC 9457 Problem Details: Khi service ném exception,
@ExceptionHandlertại tầng@ControllerAdvicesẽ đóng gói lỗi thành đối tượngProblemDetailchuẩn hóa.
8. Những điểm then chốt cần ghi nhớ là gì?
- Chọn cách trả dữ liệu phù hợp: Trả DTO trực tiếp cho GET đơn giản,
ResponseEntitykhi cần kiểm soát status và headers,@ResponseStatuscho method trả về void. - Tuân thủ ngữ nghĩa HTTP Status: 201 cho POST tạo mới, 204 cho DELETE, 202 cho tác vụ nền, và các mã 4xx/5xx tương ứng với lỗi.
- Đính kèm header
Locationkhi trả về mã 201 Created. - Không bao giờ trả mã 200 OK đi kèm body thông báo lỗi nghiệp vụ.
HttpMessageConverterchịu trách nhiệm biến đổi đối tượng Java thành định dạng phản hồi (JSON, XML) thông qua Content Negotiation.
9. Bài tập tự kiểm tra mức độ hiểu sâu
- Q1Khi thực hiện thành công thao tác tạo mới một tài nguyên qua HTTP POST, REST API chuẩn nên trả về status code và header nào?
- Q2Tại sao không bao giờ nên trả về HTTP 200 OK đi kèm JSON chứa thông tin lỗi như {'error': 'not_found'}?
- Q3Thành phần nào trong Spring MVC trực tiếp đảm nhiệm việc chuyển đổi đối tượng Java trả về từ controller thành dữ liệu byte JSON trên HTTP response?
- Q4Trường hợp nào sau đây là phù hợp nhất để sử dụng @ResponseStatus(HttpStatus.NO_CONTENT)?
- Q5Khi client gửi request với header 'If-None-Match: "v1"' và server nhận thấy phiên bản tài nguyên không hề thay đổi, server nên trả về response như thế nào?
Bài tiếp theo: Streaming, ResponseBodyAdvice và HATEOAS
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
ArgumentResolver: Chuỗi Phân Giải Tham Số Spring MVC
Bài tiếpResponse Nâng Cao: Streaming, Advice, HATEOAS và Security
Lộ trình Junior Java Backend Developer
Chặng 15/21 · REST API với Spring MVC — bài 10/21 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này