Spring Boot bài 21: REST CRUD: POST, PUT, DELETE và mã trạng thái
- 18-10-2026
- Toanngo92
- 0 Comments
Giáo trình Spring Boot MVC và REST API · Chương 9/12: REST API và hợp đồng HTTP · Bài 21/28 · Biên soạn tháng 10/2026.
Mục lục
Mục tiêu bài học
Thiết kế request tạo/sửa/xóa, trả Location khi tạo mới và kiểm tra kết quả bằng một chuỗi request hoàn chỉnh.
Tạo tài nguyên bằng POST
POST /api/courses nhận JSON có title, price và categoryId. @RequestBody yêu cầu Spring đọc body thành CourseForm; @Valid kiểm tra ràng buộc trước nghiệp vụ. Khi tạo thành công, API trả 201 Created, body là DTO vừa lưu và Location trỏ tới URL chi tiết.
Không cho client tùy chọn id khi tạo mới trong hợp đồng này. Id đến từ database. Location giúp client biết tài nguyên vừa được tạo ở đâu mà không phải tự suy đoán quy tắc URL. Ở hệ thống có gateway hoặc context path phức tạp, có thể dùng công cụ xây URI theo request và cấu hình forwarded header tin cậy.
Tệp trong mã nguồn mẫu: src/main/java/com/hocvietcode/academy/CourseApiController.java. Đoạn dưới được trích từ tệp này; xem bản tải về để có đủ import và phần còn lại.
@PostMapping("/courses") ResponseEntity<CourseDto> create(@Valid @RequestBody CourseForm form){CourseDto dto=service.create(form);return ResponseEntity.created(URI.create("/api/courses/"+dto.id())).body(dto);}
@PutMapping("/courses/{id}") CourseDto update(@PathVariable Long id,@Valid @RequestBody CourseForm form){return service.update(id,form);}
@DeleteMapping("/courses/{id}") ResponseEntity<Void> delete(@PathVariable Long id){service.delete(id);return ResponseEntity.noContent().build();}
PUT cập nhật đầy đủ phần được phép sửa
PUT /api/courses/{id} trong mẫu yêu cầu đủ ba trường được sửa, không phải JSON tùy ý chỉ có một trường. Nếu bỏ title hoặc categoryId, validation từ chối. Endpoint không có chức năng upsert: id không tồn tại trả 404. Hãy ghi rõ lựa chọn này trong hợp đồng để client không mặc định PUT sẽ tự tạo mới.
PATCH dành cho cập nhật một phần cần thiết kế riêng, bao gồm cách biểu diễn trường không gửi và trường muốn xóa. Không biến PUT thành PATCH một cách ngầm định bằng cách bỏ qua mọi giá trị null; điều đó làm hợp đồng khó dự đoán.
DELETE và tính idempotent
DELETE /api/courses/{id} trả 204 No Content sau khi xóa. Response 204 không cần thêm JSON thông báo. Gọi lại cùng id trong mẫu trả 404 vì tài nguyên đã mất. Idempotent nói về trạng thái đích khi thao tác lặp lại, không bắt buộc mọi lần phải trả cùng mã status.
POST tạo mới thông thường không idempotent: gửi hai lần có thể tạo hai khóa học. Với nghiệp vụ quan trọng cần chống lặp, phải có thiết kế khóa/ràng buộc/idempotency riêng. Không dựa vào việc UI khóa nút để đảm bảo chỉ có một request.
Chạy một vòng CRUD có xác thực và CSRF
Dùng quy trình lấy token và session ở bài 19, rồi POST JSON tạo khóa học. Lấy Location từ response, GET để kiểm tra, PUT toàn bộ dữ liệu mới, GET lại và cuối cùng DELETE. GET lần cuối phải trả 404. Mỗi request ghi đều cần teacher và token hợp lệ.
Nếu gặp 415, kiểm tra Content-Type và định dạng body; 400 thường là JSON/validation/tham số; 403 cần kiểm tra quyền hoặc CSRF; 404 có thể là id không tồn tại hoặc URL sai. Không sửa nghiệp vụ xóa khi request chưa qua được tầng Security. Test crudRoundTrip trong mã mẫu thực hiện chính chuỗi này trong transaction kiểm thử.
Tệp trong mã nguồn mẫu: src/test/java/com/hocvietcode/academy/ApiIntegrationTest.java. Đoạn dưới được trích từ tệp này; xem bản tải về để có đủ import và phần còn lại.
@Test void crudRoundTrip()throws Exception{
String location=mvc.perform(post("/api/courses").with(user("t").roles("TEACHER")).with(csrf()).contentType("application/json").content(JSON))
.andExpect(status().isCreated()).andReturn().getResponse().getHeader("Location");
mvc.perform(put(location).with(user("t").roles("TEACHER")).with(csrf()).contentType("application/json").content(JSON.replace("REST demo","Updated")))
.andExpect(status().isOk()).andExpect(jsonPath("$.title").value("Updated"));
mvc.perform(delete(location).with(user("t").roles("TEACHER")).with(csrf())).andExpect(status().isNoContent());
mvc.perform(get(location)).andExpect(status().isNotFound());
}
Lỗi thường gặp và lưu ý
- Không nhận một entity có id từ client rồi save mà chưa xác định đang tạo hay cập nhật tài nguyên nào.
- 204 không có body; đừng bắt client parse JSON sau một DELETE thành công theo hợp đồng này.
Ghi nhớ
CRUD API cần thống nhất URL, method, đầu vào, status và quyền; một chuỗi kiểm tra trọn vòng sẽ phát hiện nhiều lỗi hơn từng request riêng lẻ.

