Skip to content
PickNTech

Building REST APIs with Spring Boot

Design and build a CRUD REST API with proper routes, status codes and validation.

Intermediate25 minUpdated 24 Sept 2026

What you’ll learn

  • REST resource and route design
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
  • Returning the right HTTP status codes
  • Validating request bodies and handling errors

Designing the routes

REST APIs are organised around resources (nouns) and use HTTP methods (verbs) to act on them. For an employee API:

MethodRouteMeaningSuccess status
GET/api/employeesList employees200 OK
GET/api/employees/42Get one200 OK
POST/api/employeesCreate201 Created
PUT/api/employees/42Replace200 OK
DELETE/api/employees/42Delete204 No Content

Request and response bodies

Use records as DTOs — they're concise and immutable:

public record EmployeeRequest(
        @NotBlank String name,
        @Email String email,
        @Positive long salary) {}
 
public record EmployeeResponse(long id, String name, String email) {}

The validation annotations come from spring-boot-starter-validation.

The controller

EmployeeController.java
@RestController
@RequestMapping("/api/employees")
public class EmployeeController {
 
    private final EmployeeService service;
 
    public EmployeeController(EmployeeService service) {
        this.service = service;
    }
 
    @GetMapping
    public List<EmployeeResponse> list() {
        return service.findAll();
    }
 
    @GetMapping("/{id}")
    public EmployeeResponse get(@PathVariable long id) {
        return service.findById(id); // throws EmployeeNotFoundException
    }
 
    @PostMapping
    public ResponseEntity<EmployeeResponse> create(@Valid @RequestBody EmployeeRequest req) {
        EmployeeResponse created = service.create(req);
        URI location = URI.create("/api/employees/" + created.id());
        return ResponseEntity.created(location).body(created);
    }
 
    @PutMapping("/{id}")
    public EmployeeResponse update(@PathVariable long id, @Valid @RequestBody EmployeeRequest req) {
        return service.update(id, req);
    }
 
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable long id) {
        service.delete(id);
    }
}

Spring converts records to JSON (and back) automatically using Jackson.

HTTP request→DispatcherServlet→@Valid + JSON → record→Controller→Service→JSON response

Handling errors in one place

Don't catch exceptions in every controller. A @RestControllerAdvice maps exceptions to HTTP responses globally:

@RestControllerAdvice
public class ApiExceptionHandler {
 
    @ExceptionHandler(EmployeeNotFoundException.class)
    public ProblemDetail notFound(EmployeeNotFoundException ex) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail invalid(MethodArgumentNotValidException ex) {
        var pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        pd.setDetail("Validation failed");
        pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .toList());
        return pd;
    }
}

ProblemDetail produces the standard RFC 9457 error format, so clients get consistent errors.

Common mistake

Never return your JPA entities directly from controllers. You'll leak internal fields, create lazy-loading surprises and couple your API to your database schema. Map to response DTOs.

⚡ Try it yourself

Implement EmployeeService with an in-memory Map<Long, EmployeeResponse> and an AtomicLong for IDs. Test every endpoint with curl or an HTTP client, including a POST with an invalid email to see your validation error.

Interview questions

QWhat's the difference between @Controller and @RestController?+

@RestController is @Controller plus @ResponseBody on every method, so return values are written directly to the response body (usually as JSON) instead of being resolved as view names.

QPUT vs PATCH?+

PUT replaces the whole resource with the request body and is idempotent. PATCH applies a partial update to specific fields.

Key takeaways

  • Resources are nouns; HTTP methods are the verbs.
  • Return correct status codes: 201 for create, 204 for delete.
  • Validate with @Valid and handle errors centrally with @RestControllerAdvice.

Finished this lesson?

Mark it complete to move forward on your roadmap.