Building REST APIs
@RestController, request mapping, validation, exception handling, HATEOAS
Spring MVC provides the @RestController model for building HTTP APIs. Understanding request mapping, input validation, exception handling, and response shaping lets you build clean, predictable REST APIs that are easy to consume and maintain.
Key Points
- @RestController: combines @Controller + @ResponseBody — all methods return response body (JSON by default via Jackson)
- @RequestMapping / @GetMapping / @PostMapping / @PutMapping / @DeleteMapping / @PatchMapping: map HTTP method + path
- @PathVariable: bind URL path segment to parameter — /users/{id} → @PathVariable Long id
- @RequestParam: bind query parameter — /search?q=alice&page=0 → @RequestParam String q, @RequestParam(defaultValue="0") int page
- @RequestBody: deserialise JSON body to object; @ResponseBody / ResponseEntity<T> for response control
- Bean Validation: @Valid on @RequestBody triggers validation; @NotNull, @Size, @Email, @Min, @Max on fields
- @ControllerAdvice + @ExceptionHandler: global exception handler — map exceptions to HTTP status codes
- ResponseEntity<T>: full control over status code, headers, and body — return ResponseEntity.ok(body) or .status(HttpStatus.CREATED).body(body)
- Content negotiation: Spring negotiates response format (JSON/XML) based on Accept header
Spring REST: record DTOs, pagination, ResponseEntity, @Valid validation, @RestControllerAdvice with ProblemDetail (RFC 7807)
// Request/Response DTOs — separate from domain model
public record CreateUserRequest(
@NotBlank @Size(max = 100) String name,
@Email String email,
@Min(0) @Max(150) int age
) {}
public record UserResponse(Long id, String name, String email) {}
// REST controller
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService service;
@GetMapping
public Page<UserResponse> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return service.findAll(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> get(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {
return service.create(req);
}
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
var detail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
detail.setDetail("Validation failed");
detail.setProperty("errors", ex.getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList());
return detail;
}
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(EntityNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
}Real-World Example
ProblemDetail (Spring 6 / Spring Boot 3) implements RFC 7807 — standardised error responses with type, title, status, detail, instance fields. Consumers can reliably parse errors without custom error contracts. Always use DTOs (not domain entities) as request/response types — exposing JPA entities directly leaks database schema and invites unintended data exposure.