Introduction
REST API is the most common method of communication in modern backend applications. Spring Boot provides Spring Web MVC — a powerful and mature framework for building RESTful APIs. This article will guide from basic to advanced techniques when working with Controllers.
1. HTTP Methods & REST Conventions
1.1 RESTful Resource Mapping
Resource: /api/users
GET /api/users → Lấy danh sách users
GET /api/users/{id} → Lấy user theo ID
POST /api/users → Tạo user mới
PUT /api/users/{id} → Cập nhật toàn bộ user
PATCH /api/users/{id} → Cập nhật một phần user
DELETE /api/users/{id} → Xóa user
1.2 Important HTTP Status Codes
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | GET, PUT, PATCH successful |
| 201 | Created | POST created resource successfully |
| 204 | No Content | DELETE successful |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Unverified |
| 403 | Forbidden | No permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate data (duplicate email) |
| 500 | Internal Server Error | Server error |
2. @RestController & Request Mapping
2.1 Basic Controller
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// GET /api/v1/products
@GetMapping
public List<Product> getAllProducts() {
return productService.findAll();
}
// GET /api/v1/products/123
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id);
}
// POST /api/v1/products
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Product createProduct(@RequestBody Product product) {
return productService.create(product);
}
// PUT /api/v1/products/123
@PutMapping("/{id}")
public Product updateProduct(@PathVariable Long id,
@RequestBody Product product) {
return productService.update(id, product);
}
// DELETE /api/v1/products/123
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id) {
productService.delete(id);
}
}
2.2 @RestController vs @Controller
// @RestController = @Controller + @ResponseBody
// Mọi method tự động serialize return value thành JSON
@RestController // Dùng cho REST API
public class ApiController {
@GetMapping("/api/data")
public Map<String, String> getData() {
return Map.of("key", "value"); // → JSON response
}
}
@Controller // Dùng cho server-side rendering (Thymeleaf)
public class WebController {
@GetMapping("/home")
public String home(Model model) {
model.addAttribute("title", "Home");
return "home"; // → Render template home.html
}
}
3. Receive data from Request
3.1 Path Variables
@GetMapping("/users/{userId}/orders/{orderId}")
public Order getOrder(
@PathVariable Long userId,
@PathVariable("orderId") Long orderId) { // Custom name mapping
return orderService.findByUserAndId(userId, orderId);
}
// Optional path variable
@GetMapping({"/files", "/files/{filename}"})
public String getFile(
@PathVariable(required = false) String filename) {
return filename != null ? filename : "index.html";
}
3.2 Query Parameters
// GET /api/products?category=electronics&minPrice=100&maxPrice=500
@GetMapping
public List<Product> searchProducts(
@RequestParam String category,
@RequestParam(defaultValue = "0") double minPrice,
@RequestParam(defaultValue = "999999") double maxPrice,
@RequestParam(required = false) String brand) {
return productService.search(category, minPrice, maxPrice, brand);
}
// Nhận tất cả params dưới dạng Map
@GetMapping("/search")
public List<Product> search(@RequestParam Map<String, String> params) {
return productService.searchByParams(params);
}
3.3 Request Headers
@GetMapping("/api/profile")
public UserProfile getProfile(
@RequestHeader("Authorization") String authHeader,
@RequestHeader(value = "Accept-Language", defaultValue = "vi") String lang) {
String token = authHeader.replace("Bearer ", "");
return userService.getProfile(token, lang);
}
3.4 Request Body
// POST /api/users
// Content-Type: application/json
// Body: {"name": "Duy", "email": "[email protected]"}
@PostMapping
public User createUser(@RequestBody CreateUserRequest request) {
return userService.create(request);
}
// Record class cho request
public record CreateUserRequest(
String name,
String email,
String password
) {}
4. ResponseEntity — Response control
4.1 Basics
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product created = productService.create(product);
URI location = URI.create("/api/v1/products/" + created.getId());
return ResponseEntity
.created(location) // 201 Created + Location header
.body(created);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
4.2 Custom Headers
@GetMapping("/download/{fileId}")
public ResponseEntity<byte[]> downloadFile(@PathVariable String fileId) {
FileData file = fileService.getFile(fileId);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.name() + "\"")
.header(HttpHeaders.CONTENT_TYPE, file.contentType())
.header("X-File-Size", String.valueOf(file.size()))
.body(file.data());
}
5. Content Negotiation
5.1 Produces & Consumes
@RestController
@RequestMapping("/api/v1/reports")
public class ReportController {
// Chỉ chấp nhận JSON input, trả về JSON
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public Report createJsonReport(@RequestBody ReportRequest request) {
return reportService.generate(request);
}
// Trả về CSV
@GetMapping(value = "/{id}/csv", produces = "text/csv")
public String exportCsv(@PathVariable Long id) {
return reportService.exportCsv(id);
}
}
6. API Versioning Strategies
6.1 URL Path Versioning (Recommended)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1Response getUser(@PathVariable Long id) { }
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2Response getUser(@PathVariable Long id) { }
}
6.2 Header Versioning
@GetMapping(value = "/users/{id}", headers = "X-API-Version=1")
public UserV1Response getUserV1(@PathVariable Long id) { }
@GetMapping(value = "/users/{id}", headers = "X-API-Version=2")
public UserV2Response getUserV2(@PathVariable Long id) { }
7. Real-life example: Complete CRUD API
@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping
public ResponseEntity<List<TaskResponse>> getAllTasks(
@RequestParam(defaultValue = "all") String status) {
List<TaskResponse> tasks = taskService.findByStatus(status);
return ResponseEntity.ok(tasks);
}
@GetMapping("/{id}")
public ResponseEntity<TaskResponse> getTask(@PathVariable Long id) {
return taskService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<TaskResponse> createTask(
@RequestBody CreateTaskRequest request) {
TaskResponse task = taskService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(task.id())
.toUri();
return ResponseEntity.created(location).body(task);
}
@PatchMapping("/{id}")
public ResponseEntity<TaskResponse> updateTask(
@PathVariable Long id,
@RequestBody UpdateTaskRequest request) {
return taskService.update(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
taskService.delete(id);
return ResponseEntity.noContent().build();
}
}
// Request/Response records
public record CreateTaskRequest(String title, String description) {}
public record UpdateTaskRequest(String title, String description, String status) {}
public record TaskResponse(Long id, String title, String description,
String status, LocalDateTime createdAt) {}
Summary
- @RestController combines @Controller + @ResponseBody, automatically serializes response into JSON
- Use @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping for CRUD operations
- ResponseEntity allows complete control over HTTP status code, headers and body
- API versioning should use URL path (/api/v1/) for simplicity and clarity
Exercises
- Create CRUD REST API for entity
Book(id, title, author, isbn, price, publishedYear) with full HTTP methods - Implement search endpoint:
GET /api/books?author=X&minPrice=Y&maxPrice=Zwith all optional params - Use ResponseEntity to return the correct status codes (201 Created with Location header, 204 No Content for DELETE, 404 for resource does not exist)