Building REST APIs
Spring MVC makes building HTTP APIs declarative and concise. With a few annotations you map URLs to methods, bind request data, and return JSON. This page builds a complete CRUD controller from the ground up.
@RestController and request mappings
@RestController combines @Controller with @ResponseBody, so return values are serialized to the response body (JSON by default via Jackson). @RequestMapping at class level sets a common path prefix.
@RestController
@RequestMapping("/api/products")
public class ProductController {
// handlers go here
}
Each HTTP verb has a shortcut annotation: @GetMapping, @PostMapping, @PutMapping, @PatchMapping, and @DeleteMapping.
Binding request data
Spring binds incoming data through dedicated annotations.
| Annotation | Source | Example |
|---|---|---|
@PathVariable | URL path segment | /products/42 |
@RequestParam | Query string | ?page=2&size=20 |
@RequestBody | Request body (JSON) | POST payload |
@GetMapping("/{id}")
public Product byId(@PathVariable Long id) { ... }
@GetMapping
public List<Product> search(@RequestParam(defaultValue = "0") int page) { ... }
ResponseEntity and status codes
Returning a domain object yields 200 OK. To control status, headers, or send no body, return a ResponseEntity.
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/products/" + saved.getId())
.body(saved);
| Scenario | Status |
|---|---|
| Successful read | 200 OK |
| Resource created | 201 Created |
| Successful delete (no body) | 204 No Content |
| Validation failure | 400 Bad Request |
| Resource missing | 404 Not Found |
A full CRUD controller
This controller exposes create, read, update, and delete operations over a ProductService.
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public List<Product> findAll() {
return service.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Product> findById(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Product> create(@Valid @RequestBody ProductRequest body) {
Product saved = service.create(body);
return ResponseEntity.status(HttpStatus.CREATED).body(saved);
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id,
@Valid @RequestBody ProductRequest body) {
return service.update(id, body)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}
Validation with @Valid
Spring integrates with Jakarta Bean Validation. Annotate the request DTO with constraints and the parameter with @Valid.
public record ProductRequest(
@NotBlank String name,
@Positive BigDecimal price,
@Size(max = 280) String description) { }
When validation fails, Spring throws MethodArgumentNotValidException. Translate it into a clean response with @RestControllerAdvice.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handle(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return errors;
}
}
Output:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "name": "must not be blank", "price": "must be greater than 0" }
Tip: Expose DTOs (records work beautifully) rather than JPA entities in your API. This decouples your wire format from your database schema and avoids lazy-loading serialization traps.
Best Practices
- Return
ResponseEntitywhen you need to control status, headers, or empty bodies; return plain objects otherwise. - Use nouns and plural resource names in URLs (
/api/products), and HTTP verbs for actions. - Validate input with
@Validand DTOs; never trust raw request bodies. - Centralize error handling in a
@RestControllerAdvicefor consistent error responses. - Map exceptions to correct status codes,
404for missing resources,400for bad input. - Keep controllers thin; delegate all logic to services.