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ểu — n là int 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:
HttpMediaTypeNotSupportedException → 415 MethodArgumentTypeMismatchException → 400 HttpRequestMethodNotSupportedException → 405 MissingServletRequestParameterException → 400 HttpMessageNotReadableException (JSON hỏng) → 400
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)
produces và consumes 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.
Và 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.