skip to content

How do you read request data with ServerRequest and build responses with ServerResponse in a functional handler?

level: middleimportance: should knowfreq 30%

answer

  1. pathVariable / param(Optional) / body(Class)
  2. Same HttpMessageConverters as annotations
  3. ServerResponse.ok()/created(uri)/notFound() then body()/build()
  4. No auto @Valid — validate explicitly
  5. Immutable request & response

basics

~10 s

ServerRequest 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 s

In 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 lines
java
import 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

for a junior

Know body(Class), pathVariable, param, and the ServerResponse.ok().body(...) shape.

for a middle

Know param returns Optional, body uses ParameterizedTypeReference for generics, and validation is manual.

for a senior

Explain that message converters and exception resolution are shared with annotated MVC, and how to wire validation/error handling explicitly.

for a principal

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>

context