Spring REST API & Data JPA
10/46
Bài 10 / 46~12 phútRequest & ResponseLộ trình · chặng 15/21Miễn phí… lượt xem

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:

Java
@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:

Java
@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):

Java
@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ếpResponseEntity@ResponseStatus
Mã trạng thái (Status)Cố định 200Tùy biến linh hoạtCố định theo annotation
Tùy chỉnh HeaderKhôngĐầy đủKhông
Body theo điều kiệnKhôngCó thể thay đổiKhông có body (thường dùng void)
Ngữ cảnh sử dụngGET đơn giảnPOST, Conditional GET, Custom HeaderDELETE, 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ự.

Cây quyết định bốn câu hỏi cho handler chạy xong không lỗi: vừa tạo resource mới thì 201 Created kèm Location, còn chạy nền thì 202 Accepted, ETag khớp If-None-Match thì 304 Not Modified, có dữ liệu để trả thì 200 OK, không thì 204 No Content

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 Location chỉ đế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ĩaKhi nào áp dụng
400 Bad RequestCú 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 UnauthorizedChưa xác thựcKhông cung cấp token hoặc token không hợp lệ
403 ForbiddenKhông có quyền truy cậpĐã đăng nhập nhưng không đủ quyền hạn (Role/Permission)
404 Not FoundKhông tìm thấyID tài nguyên không tồn tại trong hệ thống
409 ConflictXung đột trạng tháiEmail đã đăng ký, version xung đột khi concurrent update
422 Unprocessable EntitySai 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 RequestsVượt giới hạn tần suấtClient 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:

Java
// 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

Java
@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ự:

  1. DispatcherServlet ủy quyền cho HandlerMethodReturnValueHandler xử lý giá trị trả về.
  2. Handler kiểm tra header Accept từ phía request client (ví dụ application/json).
  3. 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.
  4. MappingJackson2HttpMessageConverter được chọn, kích hoạt ObjectMapper.writeValue() để chuyển đối tượng Java thành chuỗi byte JSON.
  5. Spring ghi chuỗi bytes vào HttpServletResponse.getOutputStream() và thiết lập header Content-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?

HeaderVai tròVí dụ thực tế
LocationĐường dẫn URI đến tài nguyên mới tạo/api/v1/orders/1024
ETagMã hash xác thực phiên bản dữ liệu phục vụ caching"a8f5c3b2"
Cache-ControlChỉ thị caching cho Browser và CDNpublic, max-age=3600
Content-DispositionChỉ định tải tệp đính kèmattachment; filename="report.xlsx"
X-Total-CountTổng số bản ghi hỗ trợ phân trang150
Retry-AfterThời gian chờ (giây) trước khi client được phép gửi lại request120
Java
@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:

Java
// 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:

Java
// 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));
}

Hai cột đối chiếu cùng một sự thật không tìm thấy: cột trả 200 kèm body báo lỗi khiến axios không ném lỗi, CDN cache lại như dữ liệu hợp lệ và APM đếm là thành công nên không cảnh báo nào nổ; cột trả 404 kèm Problem Details thì cả ba đọc đúng

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, @ExceptionHandler tại tầng @ControllerAdvice sẽ đóng gói lỗi thành đối tượng ProblemDetail chuẩ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, ResponseEntity khi cần kiểm soát status và headers, @ResponseStatus cho 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 Location khi 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ụ.
  • HttpMessageConverter chị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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Khi 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?
  2. Q2
    Tạ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'}?
  3. Q3
    Thà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?
  4. Q4
    Trường hợp nào sau đây là phù hợp nhất để sử dụng @ResponseStatus(HttpStatus.NO_CONTENT)?
  5. Q5
    Khi 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.

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

Bài tiếp theo

Response Nâng Cao: Streaming, Advice, HATEOAS và Security