Map HTTP to Java methods — path variables, query params, JSON request bodies, status codes with ResponseEntity, bean validation, and clean error handling.
Why: @RestController marks a class whose return values become the HTTP response body (serialized to JSON). When @RequestMapping at class level: a shared URL prefix. The @GetMapping / @PostMapping / @PutMapping / @DeleteMapping shortcuts map one HTTP verb each.
@RestController
@RequestMapping("/api/books")
public class BookController {
@GetMapping
public List<Book> list() { ... } // GET /api/books
@PostMapping
public Book create(...) { ... } // POST /api/books
}When @PathVariable: a value that is part of the path and identifies a resource (/books/42). When @RequestParam: optional filters and options after the ? (/books?author=tolkien&page=2). Note: give params a defaultValue so they are truly optional.
@GetMapping("/{id}")
public Book getOne(@PathVariable Long id) {
return service.findById(id);
}
@GetMapping
public List<Book> search(
@RequestParam(required = false) String author,
@RequestParam(defaultValue = "0") int page) {
return service.search(author, page);
}Why @RequestBody: Spring deserializes the JSON payload into a Java object using Jackson. When a DTO (a record): accept only the fields a client may send, rather than exposing your entity directly — safer and decoupled from the database shape.
public record CreateBookRequest(String title, String author, double price) {}
@PostMapping
public Book create(@RequestBody CreateBookRequest request) {
return service.create(request); // JSON body → CreateBookRequest
}Why: returning an object always sends 200 OK. When ResponseEntity: you need a different status or headers — 201 Created with a Location header after a POST, 404 when nothing is found. Note: it gives you full control over status, headers, and body.
@PostMapping
public ResponseEntity<Book> create(@RequestBody CreateBookRequest req) {
Book saved = service.create(req);
return ResponseEntity
.created(URI.create("/api/books/" + saved.getId())) // 201
.body(saved);
}
@GetMapping("/{id}")
public ResponseEntity<Book> getOne(@PathVariable Long id) {
return service.find(id)
.map(ResponseEntity::ok) // 200
.orElse(ResponseEntity.notFound().build()); // 404
}Why: validate at the boundary so bad data never reaches your logic. Note: add the validation starter, annotate DTO fields with jakarta.validation constraints, and put @Valid on the parameter — Spring rejects an invalid body with 400 automatically.
import jakarta.validation.constraints.*;
public record CreateBookRequest(
@NotBlank String title,
@NotBlank String author,
@Positive double price
) {}
@PostMapping
public Book create(@Valid @RequestBody CreateBookRequest req) {
return service.create(req); // 400 returned if validation fails
}Why: a @RestControllerAdvice centralizes error handling so every controller returns consistent error JSON instead of stack traces. When @ExceptionHandler: map one exception type to one HTTP status. Note: this keeps try/catch out of your controllers.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 404
public Map<String, String> handleNotFound(BookNotFoundException ex) {
return Map.of("error", ex.getMessage());
}
}When server-side HTML: a traditional multi-page app instead of a JSON API — return a view name from a @Controller (not @RestController) and a template engine renders it. Note: Thymeleaf is the modern default; JSP still works but is legacy and not recommended for new apps.
@Controller // not @RestController
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("title", "Bookstore");
return "home"; // renders templates/home.html
}
}<!-- src/main/resources/templates/home.html (Thymeleaf) -->
<h1 th:text="${title}">Placeholder</h1>