How do you read request data with ServerRequest and build responses with ServerResponse in a functional handler?
answer
- pathVariable / param(Optional) / body(Class)
- Same HttpMessageConverters as annotations
- ServerResponse.ok()/created(uri)/notFound() then body()/build()
- No auto @Valid — validate explicitly
- Immutable request & response
basics
~10 sServerRequest gives you the body via request.body(Type.class), path variables via pathVariable("id"), and query params via param("q"). ServerResponse is a builder: ServerResponse.ok().body(obj) or ServerResponse.created(uri).build() to set status, headers, and body.
solid answer
~40 sIn a HandlerFunction, ServerRequest is your immutable read-side view. You extract path variables with pathVariable("id"), query params with param("q") (returns Optional) or params(), headers via headers(), and deserialize the body with body(SomeType.class) or body(new ParameterizedTypeReference<List<X>>(){}) for generics — both use the same HttpMessageConverters as annotated controllers. ServerResponse is a fluent, immutable builder for the write side: choose a status (ServerResponse.ok(), status(HttpStatus.CREATED), created(uri), notFound()), set headers/cookies/content-type, then terminate with body(obj) to serialize a payload or build() for an empty body. The returned ServerResponse is what Spring writes back. Validation and error handling are explicit here — there's no automatic @Valid; you call the validator yourself and choose the response.
code
java · 24 linesimport org.springframework.web.servlet.function.*;
import org.springframework.http.*;
import java.net.URI;
public class UserHandler {
private final UserService service;
public UserHandler(UserService service) { this.service = service; }
public ServerResponse get(ServerRequest req) {
long id = Long.parseLong(req.pathVariable("id"));
return service.find(id)
.map(u -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(u))
.orElseGet(() -> ServerResponse.notFound().build());
}
public ServerResponse create(ServerRequest req) throws Exception {
User in = req.body(User.class);
User saved = service.save(in);
URI loc = req.uriBuilder().path("/{id}").build(saved.id());
return ServerResponse.created(loc).body(saved);
}
}go deeper
Know body(Class), pathVariable, param, and the ServerResponse.ok().body(...) shape.
Know param returns Optional, body uses ParameterizedTypeReference for generics, and validation is manual.
Explain that message converters and exception resolution are shared with annotated MVC, and how to wire validation/error handling explicitly.
Reason about immutability guarantees, streaming/SSE/async ServerResponse variants, and consistency of contract behavior across functional and annotated endpoints.
## ServerRequest — the read side `ServerRequest` is an **immutable** abstraction over the incoming HTTP request. Key accessors: - **Path variables**: `request.pathVariable("id")` (throws if absent) or `request.pathVariables()` (a `Map`). Names come from the `{id}` template in the route predicate. - **Query parameters**: `request.param("q")` returns `Optional<String>`; `request.params()` returns a `MultiValueMap<String,String>`. - **Headers**: `request.headers()` returns a `ServerRequest.Headers` with typed helpers (`accept()`, `contentType()`, `header(name)`). - **Body**: `request.body(User.class)` deserializes using the registered `HttpMessageConverter`s — the *same* converters (Jackson, etc.) used by annotated controllers. For generic types use `request.body(new ParameterizedTypeReference<List<User>>() {})`. In the Servlet model, reading the body is **blocking**. - **Other**: `uri()`, `method()`, `attribute(name)`, `session()`, `principal()`, cookies. ## ServerResponse — the write side `ServerResponse` is built with a fluent, **immutable** builder. You pick a status/entry point, chain header setters, then terminate: - **Status entry points**: `ServerResponse.ok()`, `status(HttpStatus.CREATED)` / `status(201)`, `created(location)` (sets 201 + `Location`), `noContent()`, `notFound()`, `badRequest()`, `unprocessableEntity()`, `accepted()`. - **Header/config chaining**: `.contentType(MediaType.APPLICATION_JSON)`, `.header(name, values)`, `.cookie(cookie)`, `.cacheControl(...)`, `.eTag(...)`, `.lastModified(...)`. - **Terminal operations**: `.body(object)` serializes the object via message converters; `.body(object, ParameterizedTypeReference)` for generics; `.build()` produces an empty-body response; `.render(viewName, model)` renders a view template. - **Special forms**: `ServerResponse.sse(...)` for Server-Sent Events, and async variants like `ServerResponse.async(CompletableFuture)` to defer the response. ## Full CRUD-style example ```java public ServerResponse create(ServerRequest req) { User body = req.body(User.class); // deserialize User saved = service.save(body); URI location = req.uriBuilder() .path("/{id}").build(saved.id()); return ServerResponse.created(location).body(saved); // 201 + Location + JSON } public ServerResponse get(ServerRequest req) { long id = Long.parseLong(req.pathVariable("id")); return service.find(id) .map(u -> ServerResponse.ok().body(u)) .orElseGet(() -> ServerResponse.notFound().build()); } ``` ## Validation and errors (the big difference vs annotations) There is **no automatic `@Valid`** in the functional model. You invoke a `Validator` yourself: ```java User body = req.body(User.class); Errors errors = new BeanPropertyBindingResult(body, "user"); validator.validate(body, errors); if (errors.hasErrors()) { return ServerResponse.badRequest().body(errors.getAllErrors()); } ``` Exceptions thrown from a handler propagate to the `DispatcherServlet` and are handled by the configured `HandlerExceptionResolver`s (so a global `@ControllerAdvice` / `@ExceptionHandler` can still catch them), or you can handle errors locally with `onError` filter functions on the router. ## Gotchas - `body(...)` reads the request body; **calling it twice** is not generally supported (the stream is consumed). - `param(...)` returns `Optional` — don't assume presence; `pathVariable(...)` throws if the variable name doesn't exist in the route. - Everything is **immutable**: builder methods return new instances; there's no mutating the request in place (use `before` filters that return a *new* `ServerRequest`). - Body type conversion depends on registered converters and the request `Content-Type`; a missing/wrong content type yields the same 415/400 behavior as annotated controllers.
- How does body deserialization work — is it a different mechanism than @RequestBody?No, it's the same. request.body(Class) delegates to the configured HttpMessageConverters (e.g. Jackson), exactly like @RequestBody. Content negotiation and 415/400 behavior are consistent.
- How would you validate the request body in a functional handler?There's no automatic @Valid. You inject a Validator, create an Errors/BeanPropertyBindingResult, call validator.validate(body, errors), and return badRequest() if errors.hasErrors().
saying these in an interview costs you the question
- Expecting @Valid to trigger automatically in a HandlerFunction
- Trying to mutate ServerRequest in place (it's immutable)
- Reading request.body(...) more than once
- Assuming param() returns a String rather than Optional<String>