How do @PathVariable and @RequestParam behave in a WebFlux annotated controller — are they reactive, and how do you bind optional or multi-valued query params?
answer
- Path/query = plain values, never Mono
- Only the body is reactive
- required=false / defaultValue / Optional
- List<T> for ?tag=a&tag=b
- Missing required param = 400
basics
~20 sThey work exactly like in MVC and are plain (non-reactive) values — the URI and query string are already available, so you bind String/int directly. Use required=false or Optional for optional params, and List<T> for multi-valued ones.
solid answer
~40 s`@PathVariable` and `@RequestParam` in WebFlux are resolved from the request URI and query string, which are fully available up front — so these arguments are **plain synchronous values**, never wrapped in Mono/Flux (unlike the body, they don't require reading a streamed payload). The binding rules are identical to MVC: `@PathVariable String id` from a `{id}` URI template, `@RequestParam` from the query string. Optional params use `@RequestParam(required = false)`, a `defaultValue`, or `Optional<T>`/nullable Kotlin types. Multi-valued query params (`?tag=a&tag=b`) bind to `List<T>` or an array, or you can grab all of them with `@RequestParam MultiValueMap<String,String>`. Type conversion, `@DateTimeFormat`, and `@Valid` on the whole method work the same. The only truly reactive input is `@RequestBody`; path and query values are ordinary parameters.
code
java · 19 lines@RestController
@RequestMapping("/users")
class UserQueryController {
private final UserService service;
UserQueryController(UserService service) { this.service = service; }
@GetMapping("/{id}")
Mono<User> byId(@PathVariable UUID id) {
return service.find(id);
}
@GetMapping
Flux<User> search(@RequestParam String q,
@RequestParam(defaultValue = "0") int page,
@RequestParam(required = false) String sort,
@RequestParam(required = false) List<String> tag) {
return service.search(q, page, sort, tag);
}
}go deeper
Know path/query binding is identical to MVC and produces plain values.
Handle optional (required=false/defaultValue/Optional) and multi-valued (List) params; know missing required = 400.
Explain why only the body is reactive and how params feed the pipeline; mention MultiValueMap and validation.
Contrast synchronous URI resolution vs streamed body decoding and its API-design implications.
## Key insight: URI/query data is already there When a request arrives, its **path** and **query string** are part of the request line — they don't need to be read from a streamed body. So WebFlux resolves `@PathVariable` and `@RequestParam` **synchronously into plain values**. You never write `@PathVariable Mono<String> id` — that isn't a thing. The reactive wrapping only applies to the **body** (`@RequestBody Mono<T>`), which genuinely arrives as a stream. ## `@PathVariable` Binds a segment of a URI template to an argument: ```java @GetMapping("/users/{id}/orders/{orderId}") Mono<Order> get(@PathVariable String id, @PathVariable("orderId") String order) { ... } ``` - Name defaults to the parameter name (needs `-parameters` compilation) or is given explicitly. - Required by default; a missing/unmatched variable is a routing failure. - Type conversion applies (`@PathVariable long id`). - `@PathVariable Map<String,String>` captures all variables. ## `@RequestParam` Binds a query-string (or form) parameter: ```java @GetMapping("/users") Flux<User> search(@RequestParam String q, @RequestParam(defaultValue = "0") int page, @RequestParam(required = false) String sort) { ... } ``` - **Required by default** — a missing required param yields **400 Bad Request** (`ServerWebInputException`). - **Optional**: `required = false` (→ null / absent), `defaultValue = "…"`, or `Optional<String>`; in Kotlin a nullable type also makes it optional. - **Multi-valued** `?tag=a&tag=b`: bind to `List<String>` or `String[]`. - **Grab everything**: `@RequestParam MultiValueMap<String,String> all` or `@RequestParam Map<String,String> all` (last-value-wins for the plain Map). ## Type conversion & validation The same `ConversionService`/formatter machinery as MVC applies: `int`, `long`, `UUID`, enums, `@DateTimeFormat`/`@NumberFormat`. You can put `@Valid`/`@Validated` and constraint annotations (`@Min`, `@NotBlank`) on params when the controller is `@Validated`; violations become 400s. ## Reactive-specific notes - Because these are synchronous values, you use them directly to *build* the reactive pipeline: `return service.find(id).map(...)`. They're inputs to the chain, not part of it. - `ServerWebExchange`, `ServerHttpRequest`, or `WebSession` can also be injected if you need lower-level access — and a `WebSession` is obtained reactively by the framework but handed to you resolved. - Missing-value errors are `ServerWebInputException` (→ 400), the WebFlux analog of MVC's `MissingServletRequestParameterException`. ## Gotchas - Forgetting `-parameters` (Kotlin/Java) means implicit names may not resolve → name your annotations explicitly. - A single `@RequestParam String tag` against `?tag=a&tag=b` binds only the first value; use `List<String>` to capture all. - `required = false` without a default gives `null` — handle it, or prefer `Optional`/`defaultValue`. ## When to use Always, for identifying resources (path) and filtering/paging (query). They're the non-reactive, everyday inputs of a WebFlux handler; reserve reactive wrappers for the request body only.
- Why is @PathVariable a plain value while @RequestBody can be a Mono?The URI (and thus path variables and query params) arrives in the request line and is fully known immediately, so it resolves synchronously. The body is a potentially large streamed payload read over the network, so it can be exposed as a Mono/Flux to defer decoding and enable streaming.
- What HTTP status results from a missing required @RequestParam?400 Bad Request, raised as a ServerWebInputException — the WebFlux analog of MVC's MissingServletRequestParameterException.
saying these in an interview costs you the question
- Declaring @PathVariable Mono<String> or @RequestParam Mono<...>
- Thinking query params are read reactively like the body
- Binding ?tag=a&tag=b to a single String and expecting both values
- Assuming a missing required param silently becomes null