Spring Boot
Request Mapping
Request mapping binds HTTP requests to handler methods. @RequestMapping at the class level sets a base path; composed annotations (@GetMapping, @PostMapping, and so on) at the method level add the HTTP verb and sub-path. This entry covers path variables, path patterns, produces and consumes constraints, and mapping ambiguity resolution.
Class and Method Level Mapping
@RequestMapping on the class sets the base URI prefix shared by all methods. Method-level composed annotations add the verb and sub-path. Omitting the class-level annotation is valid — all paths must then be fully specified on each method.
Java
// ── Class-level base path + method-level sub-paths ───────────────────
@RestController
@RequestMapping("/api/v1/products") // all methods share this prefix
public class ProductController {
@GetMapping // GET /api/v1/products
public List<ProductResponse> findAll() { ... }
@GetMapping("/{id}") // GET /api/v1/products/{id}
public ProductResponse findById(@PathVariable Long id) { ... }
@GetMapping("/{id}/reviews") // GET /api/v1/products/{id}/reviews
public List<ReviewResponse> reviews(@PathVariable Long id) { ... }
@PostMapping // POST /api/v1/products
public ResponseEntity<ProductResponse> create(
@RequestBody @Valid CreateProductRequest req,
UriComponentsBuilder ucb) { ... }
@PutMapping("/{id}") // PUT /api/v1/products/{id}
public ProductResponse update(
@PathVariable Long id,
@RequestBody @Valid UpdateProductRequest req) { ... }
@DeleteMapping("/{id}") // DELETE /api/v1/products/{id}
public ResponseEntity<Void> delete(@PathVariable Long id) { ... }
}
// ── No class-level mapping — full path on every method ────────────────
@RestController
public class StatusController {
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP");
}
@GetMapping("/api/v1/version")
public Map<String, String> version() {
return Map.of("version", "1.0.0");
}
}Path Variables and Patterns
URI templates capture dynamic path segments with {variable} placeholders. @PathVariable binds them to method parameters. Wildcards and regex constraints refine which URIs a mapping accepts.
Java
@RestController
@RequestMapping("/api/v1")
public class CatalogController {
// ── Simple path variable ───────────────────────────────────────────
@GetMapping("/categories/{categoryId}/products/{productId}")
public ProductResponse findByCategoryAndId(
@PathVariable Long categoryId,
@PathVariable Long productId) { ... }
// ── Renamed binding ────────────────────────────────────────────────
@GetMapping("/products/{product-id}")
public ProductResponse findById(
@PathVariable("product-id") Long productId) { ... }
// ── Regex constraint — only numeric IDs ───────────────────────────
@GetMapping("/products/{id:[0-9]+}")
public ProductResponse findByNumericId(@PathVariable Long id) { ... }
// ── Wildcard: match any single path segment ────────────────────────
@GetMapping("/files/*")
public FileResponse singleSegment() { ... }
// ── Double wildcard: match any number of segments ─────────────────
@GetMapping("/files/**")
public FileResponse anyDepth(HttpServletRequest req) {
String path = (String) req
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
return fileService.find(path);
}
// ── Optional trailing slash — both /products and /products/ match ─
@GetMapping(value = {"/products", "/products/"})
public List<ProductResponse> products() { ... }
// ── Multiple explicit paths on one method ─────────────────────────
@GetMapping({"/items/{id}", "/products/{id}", "/catalogue/{id}"})
public ProductResponse findByAnyAlias(@PathVariable Long id) { ... }
}Request Parameters in Mapping
The params attribute narrows a mapping to requests that carry (or do not carry) specific query parameters or parameter values. This is useful for parameter-based API versioning or routing different operations to different methods on the same path.
Java
@RestController
@RequestMapping("/api/users")
public class UserController {
// ── Route by parameter presence ────────────────────────────────────
@GetMapping(params = "email") // GET /api/users?email=...
public UserResponse findByEmail(
@RequestParam String email) { ... }
@GetMapping(params = "username") // GET /api/users?username=...
public UserResponse findByUsername(
@RequestParam String username) { ... }
// ── Route by exact parameter value ────────────────────────────────
@GetMapping(params = "status=ACTIVE") // GET /api/users?status=ACTIVE
public List<UserResponse> findActive() { ... }
@GetMapping(params = "status=INACTIVE") // GET /api/users?status=INACTIVE
public List<UserResponse> findInactive() { ... }
// ── Route when parameter is absent ────────────────────────────────
@GetMapping(params = "!search") // GET /api/users (no search param)
public List<UserResponse> findAll(
@PageableDefault(size = 20) Pageable pageable) { ... }
// ── API versioning via parameter ──────────────────────────────────
@GetMapping(value = "/{id}", params = "version=1")
public UserResponseV1 findByIdV1(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}", params = "version=2")
public UserResponseV2 findByIdV2(@PathVariable Long id) { ... }
}Headers and Media Type Constraints
The headers attribute restricts a mapping to requests with specific header values. The produces and consumes attributes constrain the mapping by the response and request media types respectively. Spring returns 406 Not Acceptable or 415 Unsupported Media Type when the constraints are not met.
Java
@RestController
@RequestMapping("/api/v1/reports")
public class ReportController {
// ── Restrict by custom header ──────────────────────────────────────
@GetMapping(value = "/{id}",
headers = "X-Report-Format=pdf")
public ResponseEntity<Resource> getPdf(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}",
headers = "X-Report-Format=excel")
public ResponseEntity<Resource> getExcel(@PathVariable Long id) { ... }
// ── API versioning via header ──────────────────────────────────────
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public ReportResponseV1 getV1(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public ReportResponseV2 getV2(@PathVariable Long id) { ... }
// ── produces — restrict response media type ────────────────────────
@GetMapping(value = "/{id}/data",
produces = MediaType.APPLICATION_JSON_VALUE)
public ReportData getJson(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}/data",
produces = MediaType.APPLICATION_XML_VALUE)
public ReportData getXml(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}/download",
produces = "text/csv")
public ResponseEntity<StreamingResponseBody> getCsv(
@PathVariable Long id) { ... }
// ── consumes — restrict request body media type ───────────────────
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ReportResponse createFromJson(
@RequestBody @Valid CreateReportRequest req) { ... }
@PostMapping(consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
public ReportResponse createFromXml(
@RequestBody @Valid CreateReportRequest req) { ... }
}Sub-Resource Controllers
For nested resources — such as /users/{userId}/orders — keep the sub-resource controller separate from the parent controller. Inject both the parent and child services to resolve the relationship and validate that the parent exists before accessing the child.
Java
// ── Sub-resource controller: /api/v1/users/{userId}/orders ───────────
@RestController
@RequestMapping("/api/v1/users/{userId}/orders")
@RequiredArgsConstructor
public class UserOrderController {
private final UserService userService;
private final OrderService orderService;
@GetMapping
public ResponseEntity<Page<OrderResponse>> findAll(
@PathVariable Long userId,
@PageableDefault(size = 20) Pageable pageable) {
userService.assertExists(userId); // validate parent
return ResponseEntity.ok(
orderService.findByUser(userId, pageable));
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> findById(
@PathVariable Long userId,
@PathVariable Long orderId) {
userService.assertExists(userId);
return ResponseEntity.ok(
orderService.findByUserAndId(userId, orderId));
}
@PostMapping
public ResponseEntity<OrderResponse> create(
@PathVariable Long userId,
@RequestBody @Valid CreateOrderRequest req,
UriComponentsBuilder ucb) {
userService.assertExists(userId);
OrderResponse created = orderService.createForUser(userId, req);
URI location = ucb
.path("/api/v1/users/{userId}/orders/{orderId}")
.buildAndExpand(userId, created.id()).toUri();
return ResponseEntity.created(location).body(created);
}
@DeleteMapping("/{orderId}")
public ResponseEntity<Void> cancel(
@PathVariable Long userId,
@PathVariable Long orderId) {
userService.assertExists(userId);
orderService.cancelForUser(userId, orderId);
return ResponseEntity.noContent().build();
}
}