REST Controllers and Validation
Handle GET/POST/PUT/DELETE with DTOs, validation and error handling.
What you will learn
- Write a REST controller
- Validate input
- Handle exceptions globally
A REST controller maps HTTP requests to Java methods. Good APIs use DTOs (data transfer objects) as their public contract, validate input, and return meaningful status codes and errors.
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService service;
public TaskController(TaskService service) {
this.service = service;
}
@GetMapping
public List<TaskResponse> list() {
return service.findAll();
}
@GetMapping("/{id}")
public TaskResponse get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
public ResponseEntity<TaskResponse> create(@Valid @RequestBody CreateTaskRequest req) {
TaskResponse created = service.create(req);
return ResponseEntity.created(URI.create("/api/tasks/" + created.id())).body(created);
}
@PutMapping("/{id}")
public TaskResponse update(@PathVariable Long id, @Valid @RequestBody CreateTaskRequest req) {
return service.update(id, req);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}DTOs and validation
Java records make compact, immutable DTOs. Validation annotations from Jakarta Validation declare the rules.
import jakarta.validation.constraints.*;
public record CreateTaskRequest(
@NotBlank(message = "title is required")
@Size(max = 100) String title,
boolean done
) {}
public record TaskResponse(Long id, String title, boolean done) {}@Valid on the request body makes Spring check the rules before your method runs. On failure it rejects the request with HTTP 400. Common annotations: @NotNull, @NotBlank, @Email, @Min, @Max, @Pattern.
Global error handling
Handle exceptions in one place with @RestControllerAdvice, so every error has the same JSON shape.
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.stream.Collectors;
class TaskNotFoundException extends RuntimeException {
TaskNotFoundException(Long id) { super("Task " + id + " not found"); }
}
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(TaskNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
Map<String, String> notFound(TaskNotFoundException e) {
return Map.of("error", e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
Map<String, String> invalid(MethodArgumentNotValidException e) {
return e.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(f -> f.getField(), f -> f.getDefaultMessage(), (a, b) -> a));
}
}// POST with an empty title
{"title":"title is required"}Returning database entities directly couples your API to your schema and can leak fields. Map to response DTOs, as above.
Try it yourself
Add a priority field to CreateTaskRequest that must be between 1 and 5, and confirm a request with priority 9 returns HTTP 400.
Show solution
public record CreateTaskRequest(
@NotBlank String title,
boolean done,
@Min(1) @Max(5) int priority
) {}