Spring Boot
Request Parameters
Spring Boot binds incoming HTTP data to method parameters through a set of binding annotations. @RequestParam binds query string values, @PathVariable captures URI template segments, @RequestBody deserializes the request body, @RequestHeader reads headers, and @CookieValue reads cookies. This entry covers all five, their attributes, type conversion, and common patterns.
@RequestParam — Query and Form Parameters
@RequestParam binds a query string parameter or a form field to a method parameter. Mark optional parameters with required = false and provide a defaultValue to avoid NullPointerException. For multi-value parameters, bind to List<T>.
Java
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Validated
public class ProductController {
private final ProductService productService;
// ── Required parameter ─────────────────────────────────────────────
@GetMapping("/search")
public List<ProductResponse> search(
@RequestParam String query) { // 400 if missing
return productService.search(query);
}
// ── Optional with default ──────────────────────────────────────────
@GetMapping
public Page<ProductResponse> findAll(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sort) {
return productService.findAll(category, page, size, sort);
}
// ── Renamed parameter ─────────────────────────────────────────────
@GetMapping("/by-sku")
public ProductResponse findBySku(
@RequestParam("sku_code") String sku) { // ?sku_code=ABC123
return productService.findBySku(sku);
}
// ── Multi-value: ?tag=electronics&tag=sale ────────────────────────
@GetMapping("/tagged")
public List<ProductResponse> findByTags(
@RequestParam List<String> tag) {
return productService.findByTags(tag);
}
// ── Enum binding ──────────────────────────────────────────────────
@GetMapping("/by-status")
public List<ProductResponse> findByStatus(
@RequestParam ProductStatus status) { // ?status=ACTIVE
return productService.findByStatus(status);
}
// ── Validated parameter ───────────────────────────────────────────
@GetMapping("/range")
public List<ProductResponse> findByPriceRange(
@RequestParam @DecimalMin("0.0") BigDecimal minPrice,
@RequestParam @DecimalMin("0.0") BigDecimal maxPrice) {
return productService.findByPriceRange(minPrice, maxPrice);
}
}@PathVariable — URI Template Segments
@PathVariable captures a dynamic segment from the URI path. The variable name defaults to the parameter name; use @PathVariable("name") when they differ or when the segment name contains a hyphen. Multiple variables are independent and can appear anywhere in the path template.
Java
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
// ── Single variable ────────────────────────────────────────────────
@GetMapping("/orders/{id}")
public ResponseEntity<OrderResponse> findById(
@PathVariable Long id) {
return ResponseEntity.ok(orderService.findById(id));
}
// ── Multiple variables ─────────────────────────────────────────────
@GetMapping("/users/{userId}/orders/{orderId}")
public ResponseEntity<OrderResponse> findUserOrder(
@PathVariable Long userId,
@PathVariable Long orderId) {
return ResponseEntity.ok(
orderService.findByUserAndId(userId, orderId));
}
// ── Renamed binding (hyphenated segment name) ─────────────────────
@GetMapping("/orders/{order-id}/items/{item-id}")
public ResponseEntity<OrderItemResponse> findItem(
@PathVariable("order-id") Long orderId,
@PathVariable("item-id") Long itemId) {
return ResponseEntity.ok(
orderService.findItem(orderId, itemId));
}
// ── Regex constraint ───────────────────────────────────────────────
@GetMapping("/orders/{id:[0-9]+}")
public ResponseEntity<OrderResponse> findByNumericId(
@PathVariable Long id) {
return ResponseEntity.ok(orderService.findById(id));
}
// ── String path variable ───────────────────────────────────────────
@GetMapping("/orders/ref/{reference}")
public ResponseEntity<OrderResponse> findByReference(
@PathVariable String reference) {
return ResponseEntity.ok(
orderService.findByReference(reference));
}
}@RequestBody — Deserializing the Request Body
@RequestBody reads and deserializes the HTTP request body using the registered HttpMessageConverter. Combine it with @Valid to trigger Bean Validation. For optional bodies or partial updates, use required = false — but prefer a dedicated PATCH DTO with all fields nullable.
Java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// ── Required body + validation ─────────────────────────────────────
@PostMapping
public ResponseEntity<UserResponse> create(
@RequestBody @Valid CreateUserRequest request,
UriComponentsBuilder ucb) {
UserResponse created = userService.create(request);
return ResponseEntity.created(
ucb.path("/api/v1/users/{id}")
.buildAndExpand(created.id()).toUri())
.body(created);
}
// ── Full replacement ───────────────────────────────────────────────
@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(
@PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
// ── Partial update — all fields nullable ──────────────────────────
@PatchMapping("/{id}")
public ResponseEntity<UserResponse> patch(
@PathVariable Long id,
@RequestBody @Valid PatchUserRequest request) {
return ResponseEntity.ok(userService.patch(id, request));
}
// ── Raw Map for dynamic / schema-less payloads ────────────────────
@PatchMapping("/{id}/metadata")
public ResponseEntity<Void> patchMetadata(
@PathVariable Long id,
@RequestBody Map<String, Object> metadata) {
userService.patchMetadata(id, metadata);
return ResponseEntity.noContent().build();
}
// ── List body ─────────────────────────────────────────────────────
@PostMapping("/batch")
public ResponseEntity<List<UserResponse>> createBatch(
@RequestBody @Valid List<CreateUserRequest> requests) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(userService.createBatch(requests));
}
}@RequestHeader — Reading HTTP Headers
@RequestHeader binds a named HTTP header to a method parameter. Mark optional headers with required = false to avoid 400 errors when the client omits them. To read all headers at once, bind to HttpHeaders or Map<String, String>.
Java
@RestController
@RequestMapping("/api/v1/events")
@RequiredArgsConstructor
public class EventController {
private final EventService eventService;
// ── Required header ────────────────────────────────────────────────
@PostMapping("/webhook")
public ResponseEntity<Void> webhook(
@RequestHeader("X-Webhook-Secret") String secret,
@RequestBody WebhookPayload payload) {
eventService.processWebhook(secret, payload);
return ResponseEntity.ok().build();
}
// ── Optional header with default ──────────────────────────────────
@GetMapping
public List<EventResponse> findAll(
@RequestHeader(value = "X-Tenant-ID",
required = false,
defaultValue = "default") String tenantId) {
return eventService.findAll(tenantId);
}
// ── Standard headers ──────────────────────────────────────────────
@GetMapping("/{id}")
public ResponseEntity<EventResponse> findById(
@PathVariable Long id,
@RequestHeader(value = HttpHeaders.IF_NONE_MATCH,
required = false) String etag,
@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE,
required = false,
defaultValue = "en") String lang) {
EventResponse event = eventService.findById(id, lang);
String currentEtag = """ + event.version() + """;
if (currentEtag.equals(etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
}
return ResponseEntity.ok().eTag(currentEtag).body(event);
}
// ── All headers as a map ───────────────────────────────────────────
@PostMapping("/debug")
public Map<String, String> debug(
@RequestHeader Map<String, String> headers) {
return headers;
}
// ── Bind to HttpHeaders for typed access ──────────────────────────
@GetMapping("/typed-headers")
public ResponseEntity<Void> typedHeaders(
@RequestHeader HttpHeaders headers) {
MediaType contentType = headers.getContentType();
List<Locale> langs = headers.getAcceptLanguage()
.stream().map(r -> Locale.forLanguageTag(r.getRange())).toList();
return ResponseEntity.ok().build();
}
}@ModelAttribute and @CookieValue
@ModelAttribute binds multiple query parameters or form fields to a single object by field name — ideal for filter DTOs. @CookieValue reads a named cookie from the request. Both support required and defaultValue attributes.
Java
// ── @ModelAttribute — binds query params to a DTO ────────────────────
@GetMapping("/search")
public Page<ProductResponse> search(
@ModelAttribute @Valid ProductFilter filter,
@PageableDefault(size = 20) Pageable pageable) {
// GET /products/search?category=books&minPrice=10&maxPrice=50
// Spring maps each query param to the matching field in ProductFilter
return productService.search(filter, pageable);
}
@Data
public class ProductFilter {
private String category;
@DecimalMin("0") private BigDecimal minPrice;
@DecimalMin("0") private BigDecimal maxPrice;
private ProductStatus status;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate addedAfter;
}
// ── @CookieValue — read a named cookie ────────────────────────────────
@GetMapping("/preferences")
public UserPreferences getPreferences(
@CookieValue(value = "user_prefs",
required = false) String prefsJson) {
if (prefsJson == null) return UserPreferences.defaults();
return userService.parsePreferences(prefsJson);
}
// ── Track session with a cookie ────────────────────────────────────────
@GetMapping("/cart")
public CartResponse getCart(
@CookieValue(value = "session_id",
required = false,
defaultValue = "") String sessionId,
HttpServletResponse response) {
if (sessionId.isEmpty()) {
sessionId = UUID.randomUUID().toString();
Cookie cookie = new Cookie("session_id", sessionId);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setMaxAge(7 * 24 * 3600); // 7 days
response.addCookie(cookie);
}
return cartService.getOrCreate(sessionId);
}