Controllers & Request Mapping

@GetMapping and friends, path variables, request parameters, and content negotiation basics.

Mapping annotations

@RequestMapping is the general-purpose mapping annotation, but in practice you almost always reach for one of its method-specific shortcuts instead:

Java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping                       // GET  /api/orders
    public List<Order> getAll() { ... }

    @GetMapping("/{id}")              // GET  /api/orders/{id}
    public Order getOne(@PathVariable Long id) { ... }

    @PostMapping                      // POST /api/orders
    public Order create(@RequestBody CreateOrderRequest request) { ... }

    @PutMapping("/{id}")              // PUT  /api/orders/{id}
    public Order update(@PathVariable Long id, @RequestBody UpdateOrderRequest request) { ... }

    @DeleteMapping("/{id}")           // DELETE /api/orders/{id}
    public void delete(@PathVariable Long id) { ... }

    @PatchMapping("/{id}/status")     // PATCH /api/orders/{id}/status
    public Order updateStatus(@PathVariable Long id, @RequestBody StatusUpdate update) { ... }
}

@GetMapping("/{id}") is shorthand for @RequestMapping(method = RequestMethod.GET, path = "/{id}") — the shortcuts exist purely for readability, and are what you should default to. The class-level @RequestMapping("/api/orders") establishes a base path that every method-level mapping is nested under.

Path variables

A path variable captures a dynamic segment of the URL:

Java
@GetMapping("/{id}")
public Order getOne(@PathVariable Long id) {
    return orderService.findById(id);
}

When the method parameter name matches the placeholder, @PathVariable needs no extra argument. If they differ, name it explicitly:

Java
@GetMapping("/{orderId}/items/{itemId}")
public OrderItem getItem(@PathVariable("orderId") Long orderId, @PathVariable("itemId") Long itemId) {
    return orderService.findItem(orderId, itemId);
}

Request parameters

@RequestParam reads a query string parameter (?key=value):

Java
@GetMapping("/search")
public List<Order> search(
    @RequestParam String status,
    @RequestParam(required = false) String customerName,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size
) {
    return orderService.search(status, customerName, page, size);
}
Bash
curl "localhost:8080/api/orders/search?status=SHIPPED&page=1&size=10"

By default, @RequestParam is required — a missing parameter produces a 400 Bad Request automatically. Mark it required = false (or give it a primitive-incompatible wrapper type like Integer instead of int) for genuinely optional parameters.

Content negotiation basics

Spring MVC decides which representation to send back based on the client's Accept header, and which representation to accept based on the client's Content-Type header — this is content negotiation. With Jackson on the classpath (as it is via spring-boot-starter-web), JSON is the default for both directions with no extra configuration.

Java
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Order getOne(@PathVariable Long id) {
    return orderService.findById(id);
}

produces restricts which Accept values this method will respond to; consumes restricts which Content-Type a request body must have:

Java
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public Order create(@RequestBody CreateOrderRequest request) {
    return orderService.create(request);
}

If a client requests a representation the method doesn't produce (e.g. Accept: application/xml against a JSON-only endpoint), Spring MVC responds 406 Not Acceptable instead of guessing.

Common mistakes

  • Making a parameter @RequestParam required when it's genuinely optional, causing valid requests to fail with 400 unexpectedly.
  • Relying on parameter name matching for @PathVariable/@RequestParam without realizing that compiling without the -parameters flag can break this — Spring Boot's default build setup handles this correctly, but it's worth knowing why an unusual build configuration might need explicit names.
  • Not setting consumes/produces on an endpoint that genuinely only supports one representation, leaving it to accept content types it can't actually process correctly.
  • Overloading one method to handle multiple, unrelated HTTP methods manually instead of using the dedicated @GetMapping/@PostMapping/etc. shortcuts.