Bài này về lớp mỏng giữa HTTP và mã Java của bạn.

Ánh xạ cơ bản

@RestController
public class DonController {

    @GetMapping("/don/{id}")
    Ket lay(@PathVariable String id, @RequestParam(defaultValue = "1") int n) {
        return new Ket(id, n);
    }
}
  GET /don/DH-9?n=5  ->  {"ma":"DH-9","soLuong":5}

@PathVariable lấy phần trong {}, @RequestParam lấy query string. Cả hai tự chuyển kiểunint và Spring parse hộ.

Năm chú thích theo method: @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping. Chúng là dạng rút gọn của @RequestMapping(method = ...).

Gom tiền tố chung ở mức lớp:

@RestController
@RequestMapping("/api/v1/don")
class DonController {
    @GetMapping("/{id}") ...       // /api/v1/don/{id}
}

Tham số tuỳ chọn

@RequestParam(required = false) String loc          // null nếu thiếu
@RequestParam(defaultValue = "1") int trang         // 1 nếu thiếu
Optional<String> loc                                 // Optional.empty()

@RequestParam mặc định là bắt buộc — thiếu thì Spring trả 400 trước khi vào method của bạn. Đây là hành vi đúng và bạn không cần kiểm null.

Request body

@PostMapping("/don")
ResponseEntity<Ket> tao(@Valid @RequestBody Don d) { ... }

@RequestBody chuyển JSON thành đối tượng qua Jackson. Với record, không cần chú thích gì thêm — bài 15 sẽ nói kỹ.

ResponseEntity khi cần kiểm soát

return ResponseEntity.status(HttpStatus.CREATED)
        .header("Location", "/don/" + d.ma())
        .body(new Ket(d.ma(), d.soLuong()));
  HTTP/1.1 201
  Location: /don/DH-1
  Content-Type: application/json

Trả về đối tượng trần thì luôn là 200. Cần mã khác hoặc header thì dùng ResponseEntity.

Hoặc @ResponseStatus(HttpStatus.CREATED) trên method nếu mã cố định và không cần header — gọn hơn.

Với 201, Location là header nên có: nó cho client biết tài nguyên vừa tạo nằm ở đâu.

Spring đã ánh xạ lỗi sẵn

  sai Content-Type       -> HTTP/1.1 415
  sai kiểu tham số (n=abc) -> HTTP/1.1 400

Đây là điểm tôi muốn nhấn mạnh. Spring đã ánh xạ đúng mã cho các lỗi chuẩn:

HttpMediaTypeNotSupportedException415 MethodArgumentTypeMismatchException400 HttpRequestMethodNotSupportedException405 MissingServletRequestParameterException400 HttpMessageNotReadableException (JSON hỏng) → 400

Đừng bắt Exception trong @ControllerAdvice rồi trả 500 cho tất cả. Bài 60 sê-ri Java đã đo được năm trường hợp bị đôn nhầm lên 500 vì lý do này. Bài mai sẽ nói cách xử lý đúng.

Ánh xạ nội dung

@GetMapping(value = "/don/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/don", consumes = MediaType.APPLICATION_JSON_VALUE)

producesconsumes tham gia vào việc chọn handler: hai method cùng URL nhưng khác produces thì Spring chọn theo header Accept của client.

Ít dùng trong API JSON thuần, nhưng hữu ích khi cần trả cả JSON lẫn CSV từ cùng một endpoint.

Các chú thích tham số khác

@RequestHeader("X-Request-ID") String ma
@CookieValue("phien") String phien
@RequestPart("tep") MultipartFile tep       // bài 19
HttpServletRequest req                       // truy cập thô, hạn chế dùng

HttpServletRequest dùng được nhưng nó khoá method của bạn vào Servlet API và làm test khó hơn. Chỉ dùng khi thật sự cần.

Trả về gì

Ket lay()                    // 200 + JSON
ResponseEntity<Ket>          // kiểm soát đầy đủ
void                         // 200, thân rỗng
List<Ket>                    // 200 + mảng JSON
ResponseEntity<Void>         // ví dụ 204 No Content cho DELETE

Với DELETE thành công, 204 No Content đúng hơn 200 với thân rỗng:

@DeleteMapping("/{id}")
ResponseEntity<Void> xoa(@PathVariable String id) {
    svc.xoa(id);
    return ResponseEntity.noContent().build();
}

Controller nên mỏng

Đây là nguyên tắc thiết kế quan trọng nhất của bài:

@PostMapping("/don")
ResponseEntity<KetQua> tao(@Valid @RequestBody TaoDonRequest r) {
    var don = svc.tao(r.toCommand());          // nghiệp vụ ở SERVICE
    return ResponseEntity.status(201).body(KetQua.tu(don));
}

Controller chỉ làm bốn việc: nhận và kiểm dữ liệu, gọi service, chuyển kết quả thành DTO, đặt mã HTTP. Không logic nghiệp vụ, không truy vấn CSDL.

dùng DTO riêng cho request và response, đừng lộ entity JPA ra API. Bài 24 sẽ nói vì sao — entity có quan hệ lazy và việc serialize nó dẫn thẳng tới LazyInitializationException cùng rò rỉ dữ liệu.

Thử ba mươi giây

curl -si -X POST localhost:8080/don -d "abc" | head -1
curl -si "localhost:8080/don/X?n=abc" | head -1

Nếu cả hai trả 500 thay vì 415 và 400, bạn đang có một @ExceptionHandler(Exception.class) nuốt hết — và bài mai là về cách sửa.

Ngày mai: ràng buộc dữ liệu và validation.