Building REST APIs

1. Creating REST Controllers (@RestController)

Example: CRUD REST controller

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
  private final ProductService svc;
  public ProductController(ProductService svc) { this.svc = svc; }

  @GetMapping public List<ProductDto> list() { return svc.findAll(); }
  @GetMapping("/{id}") public ProductDto byId(@PathVariable long id) { return svc.find(id); }
  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public ProductDto create(@Valid @RequestBody NewProduct body) { return svc.create(body); }
}
Annotation Effect
@RestController @Controller + @ResponseBody
@RequestMapping Class-level base path
@CrossOrigin Per-controller CORS

2. Mapping HTTP Requests (@RequestMapping)

Attribute Description
path / value URI patterns (Ant-style)
method HTTP verbs
params Required/forbidden query params
headers Conditional on headers
consumes Content-Type constraint
produces Accept constraint

3. Using Specific HTTP Methods

Annotation HTTP Verb Idempotent?
@GetMapping GET Yes
@PostMapping POST No
@PutMapping PUT Yes
@PatchMapping PATCH No
@DeleteMapping DELETE Yes

4. Handling Path Variables (@PathVariable)

Example: Multiple path variables

@GetMapping("/users/{userId}/orders/{orderId}")
public OrderDto get(@PathVariable Long userId,
                    @PathVariable("orderId") UUID id) { /* … */ }
Feature Notes
Type conversion Auto for primitives, UUID, enums
required=false Optional path var (rare)
Regex constraint {id:\d+}

5. Handling Query Parameters (@RequestParam)

Syntax Behavior
@RequestParam String q Required
@RequestParam(required=false) String q Optional
@RequestParam(defaultValue="10") int size Default
@RequestParam List<String> tags Repeated / CSV params
@RequestParam Map<String,String> all All query params

6. Handling Request Body (@RequestBody)

Example: Validated request body with Location header

public record NewUser(@NotBlank String email, @Size(min=8) String password) {}

@PostMapping
public ResponseEntity<Void> create(@Valid @RequestBody NewUser body) {
  long id = service.register(body);
  return ResponseEntity.created(URI.create("/api/users/" + id)).build();
}

7. Returning Response Entity (ResponseEntity<T>)

Builder Use
ResponseEntity.ok(body) 200 with body
ResponseEntity.status(201).body(x) Custom status
ResponseEntity.created(uri).build() 201 + Location
ResponseEntity.noContent().build() 204
ResponseEntity.notFound().build() 404
.headers(h).cacheControl(cc) Header customization

8. Setting Response Status (@ResponseStatus)

Example: @ResponseStatus on method and exception

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductDto create(@RequestBody NewProduct p) { /* … */ }

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Order not found")
public class OrderNotFoundException extends RuntimeException {}

9. Handling HTTP Headers

Element Use
@RequestHeader("X-API-Key") String key Single header
@RequestHeader Map<String,String> All headers
@RequestHeader HttpHeaders headers Typed
ResponseEntity.ok().header("X-Total", "42") Set response header

10. Using Content Negotiation

Strategy Configuration
Accept header Default — most RESTful
Path extension Disabled by default since 5.3
Query param spring.mvc.contentnegotiation.favor-parameter=true
Producible types @GetMapping(produces={"application/json","application/xml"})
@GetMapping("/{id}")
public EntityModel<Order> one(@PathVariable Long id) {
  Order o = repo.findById(id).orElseThrow();
  return EntityModel.of(o,
    linkTo(methodOn(OrderController.class).one(id)).withSelfRel(),
    linkTo(methodOn(OrderController.class).all()).withRel("orders"));
}
API Use
EntityModel<T> Single resource + links
CollectionModel<T> Collection + links
RepresentationModelAssembler Reusable assembler

12. Versioning REST APIs

Strategy Example Trade-off
URI path /api/v1/users Simple, cache-friendly
Query param ?v=2 Easy but ugly
Header X-API-Version: 2 Clean URLs, less discoverable
Media type application/vnd.api.v2+json True REST, harder for clients