Learn / Frameworks / Spring Boot / REST Controllers and Validation

Beginner 17 min

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));
    }
}
Output
// POST with an empty title
{"title":"title is required"}
Do not expose entities

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
) {}