Backend

99 Ngày Spring — Ngày 15: ResponseEntity & status code

SSite Admin
11 tháng 08, 2026 5 phút đọc 0 lượt xem
99 Ngày Spring — Ngày 15: ResponseEntity & status code

API của ta nhận request đã chuẩn (Ngày 12–14), nhưng trả lời thì vẫn quê: tạo mới xong trả 200 trống trơn, tìm không thấy cũng… 200. Trong HTTP, status code là câu đầu tiên của mọi câu trả lời — máy móc (cache, monitoring, client SDK) đọc nó trước cả body. Hôm nay ta học nói cho đúng: bảng status tối thiểu, ResponseEntity để điều khiển trọn vẹn response, và nghi thức 201 Created + Location của dân REST chuyên nghiệp.

Sketchnote Ngày 15: ResponseEntity & status code — bảng mã tối thiểu, 201 Created + Location, 204 No Content

Status code — ngữ pháp của HTTP

// Bảng status code tối thiểu cho REST API tử tế:
//
// 2xx — THÀNH CÔNG
//   200 OK          đọc/sửa thành công (GET, PUT)
//   201 Created     TẠO MỚI thành công (POST) — kèm header Location!
//   204 No Content  thành công, không có gì để trả (DELETE)
//
// 4xx — LỖI PHÍA CLIENT
//   400 Bad Request       body/tham số sai (validation Ngày 14 trả cái này)
//   404 Not Found         tài nguyên không tồn tại
//   409 Conflict          đụng độ trạng thái (slug trùng, đặt chỗ đã kín)
//
// 5xx — LỖI PHÍA SERVER (bug, DB sập — KHÔNG phải lỗi của client)
//   500 Internal Server Error
//
// Tội kinh điển: cái gì cũng 200 — kể cả lỗi, nhét {"success": false} vào body.
// Client phải MỔ BODY mới biết hỏng; cache, monitoring, retry đều mù. ĐỪNG.
  • Ranh giới quan trọng nhất nằm giữa 4xx và 5xx: 4xx nghĩa là "bạn gửi sai — sửa rồi hãy gửi lại", 5xx nghĩa là "tôi hỏng — thử lại sau". Trộn lẫn hai loại là phá hỏng monitoring lẫn chiến lược retry của client.

  • Nhớ bảng nhỏ này là đủ 90% cuộc đời: 200 / 201 / 204 / 400 / 404 / 409 / 500 — đừng vội học thuộc cả trăm mã.

ResponseEntity — cầm lái toàn bộ response

@RestController
@RequestMapping("/api/posts")
public class PostController {

    // Trả object trần (Ngày 11) = 200 mặc định — vẫn hợp lệ khi chỉ cần thế!
    @GetMapping
    public List<PostResponse> list() { ... }

    // ResponseEntity = quyền điều khiển ĐẦY ĐỦ: status + header + body
    @GetMapping("/{id}")
    public ResponseEntity<PostResponse> detail(@PathVariable Long id) {
        return postService.find(id)                    // Optional<Post>
                .map(PostResponse::from)
                .map(ResponseEntity::ok)               // có → 200 + body
                .orElse(ResponseEntity.notFound().build());   // không → 404
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        postService.delete(id);
        return ResponseEntity.noContent().build();     // 204 — xóa là hết chuyện
    }
}
  • ResponseEntity<T> gói ba thứ: status + header + body — các factory method (ok, notFound, noContent) đọc lên thành văn.

  • Mẫu Optional → map → orElse(404) là chuẩn mực cho "tìm theo id": có thì 200, không thì 404 — không bao giờ trả 200 với body null. (Optional mổ xẻ kỹ ở series Java Ngày 58.)

  • DELETE thành công trả 204 No Content — "xong việc, không có gì để kể"; trả lại object vừa xóa là thói quen lạ cần bỏ.

201 Created — nghi thức tạo mới chuẩn REST

@PostMapping
public ResponseEntity<PostResponse> create(
        @Valid @RequestBody CreatePostRequest req) {   // Ngày 13 + 14 đứng gác

    Post post = postService.create(req);

    URI location = ServletUriComponentsBuilder
            .fromCurrentRequest()                      // /api/posts
            .path("/{id}")                             // + /42
            .buildAndExpand(post.getId())
            .toUri();

    return ResponseEntity
            .created(location)                         // 201 + Location: /api/posts/42
            .body(PostResponse.from(post));
}

// Nghi thức 201 chuẩn REST: status nói "đã tạo",
// header Location nói "nó ở ĐÂY" — client lấy id mà không cần mổ body.
// (Cùng ngày bên series Java: đa hình — PostResponse.from cũng là
//  "một hợp đồng, nhiều hình hài" đấy nhé!)
  • Ba mảnh của một lần tạo mới tử tế: status 201 (đã tạo) + header Location (địa chỉ tài nguyên mới) + body (bản ghi vừa sinh kèm id) — client muốn dùng kiểu gì cũng chiều được.

  • ServletUriComponentsBuilder.fromCurrentRequest() dựng Location từ chính URL đang gọi — không hard-code domain, đổi môi trường (Ngày 09!) không vỡ.

  • Toàn bộ dàn phòng thủ tuần này đứng chung một khung hình: @Valid (Ngày 14) gác cửa vào, DTO (Ngày 13) làm hợp đồng, hôm nay ResponseEntity chỉnh câu trả lời.

Đèn giao thông trong tuyết — status code là tín hiệu giao thông của HTTP: xanh đi tiếp, đỏ dừng lại, ai cũng đọc được ngay

Header, cache & nguyên tắc nhất quán

// Header tùy chỉnh khi cần:
return ResponseEntity.ok()
        .header("X-Total-Count", String.valueOf(total))   // ví dụ: tổng bản ghi
        .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)))  // Cache-Control
        .body(items);

// Nguyên tắc chọn kiểu trả về cho controller — giữ NHẤT QUÁN:
//  1. Chỉ cần 200 + body?           → trả object trần cho gọn
//  2. Status/header thay đổi theo logic? → ResponseEntity
//  3. Lỗi (404/409/500)?            → NÉM EXCEPTION, đừng tự build response lỗi
//     rải rác — Ngày 16: @ControllerAdvice gom xử lý lỗi về MỘT chỗ
  • Quy tắc ba nhánh trong code trên đáng dán lên màn hình — đặc biệt nhánh 3: đừng tự build response lỗi trong từng controller; ném exception và để một nơi duy nhất dịch thành HTTP — chính là bài ngày mai.

  • Đội ngũ ăn ý là đội ngũ thống nhất một kiểu: API mà chỗ trả ResponseEntity, chỗ trả object trần, chỗ 200-cho-tất là API khó tin cậy.

Bài tập nhỏ

  • Nâng cấp GET /api/books/{id} theo mẫu Optional → 404 — thử id có thật và id ma, soi status bằng curl -i.

  • Nâng cấp POST /api/books trả 201 + Location — lấy URL từ header rồi GET theo nó để khép vòng.

  • Thêm DELETE /api/books/{id} trả 204 — xóa hai lần liên tiếp: lần hai nên trả gì? (Tranh luận 204 vs 404 — chọn và bảo vệ lập luận của bạn.)

  • Gắn X-Total-Count vào endpoint danh sách — đọc header đó từ curl -i.

Kết luận

API giờ nói chuẩn ngữ pháp HTTP: bảng status tối thiểu với ranh giới 4xx/5xx rạch ròi, ResponseEntity cầm lái status–header–body, nghi thức 201 + Location cho tạo mới và 204 cho xóa, cùng nguyên tắc vàng "lỗi thì ném, đừng tự build". Nguyên tắc ấy dẫn thẳng tới Ngày 16: xử lý lỗi toàn cục@ControllerAdvice, @ExceptionHandler và chuẩn ProblemDetail (RFC 7807) gom mọi tình huống lỗi về một mối, đẹp và nhất quán. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 15: Đa hình (Polymorphism)

Ngày 15 của 99 Ngày Java: upcasting và dynamic dispatch — kiểu biến vs kiểu đối tượng thật, một vòng lặp tính lương cho mọi loại nhân viên, downcasting với instanceof & pattern matching, lập trình theo hợp đồng (nền móng của DI), và toString — đa hình bạn dùng mỗi ngày.

11 thg 8, 20266 phút1
99 Ngày Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút25
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends từ class cha, override với @Override và super., thứ tự khởi tạo từ cha trước con sau qua super(...), protected .v.v

10 thg 8, 20266 phút28