How do you register functional routes as a bean and organize handler logic separately from routing?
answer
- router config = thin, handler = logic
- method references h::getById
- @Component handler, constructor-injected service
- RouterFunctionMapping auto-detects beans
- switchIfEmpty(notFound)
basics
~10 sExpose a @Bean of type RouterFunction<ServerResponse> in a @Configuration class that only wires paths to handlers, and put the actual logic in a separate @Component 'handler' class whose methods are referenced by method reference.
solid answer
~40 sThe idiomatic split is: a router @Configuration class holds one @Bean RouterFunction<ServerResponse> that maps predicates to handler method references, and a separate @Component handler class contains the methods (each ServerRequest -> Mono<ServerResponse>). RouterFunctionMapping auto-detects the bean — no explicit registration. This keeps routing declarative and readable while handlers stay unit-testable and injectable (they can depend on services/repositories via constructor injection). You can define multiple RouterFunction beans across features; Spring combines them all. Handlers do request parsing (pathVariable, bodyToMono), call the service layer, and build a ServerResponse. Because the handler is just a bean, you test it by passing a mock ServerRequest, or test the whole router with WebTestClient.bindToRouterFunction without starting a server.
code
java · 36 linesimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.web.reactive.function.server.*;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Configuration
class PersonRouterConfig {
@Bean
RouterFunction<ServerResponse> personRoutes(PersonHandler h) {
return RouterFunctions.route()
.GET("/persons/{id}", accept(APPLICATION_JSON), h::getById)
.POST("/persons", h::create)
.build();
}
}
@Component
class PersonHandler {
private final PersonService service;
PersonHandler(PersonService service) { this.service = service; }
Mono<ServerResponse> getById(ServerRequest req) {
return service.find(req.pathVariable("id"))
.flatMap(p -> ServerResponse.ok().bodyValue(p))
.switchIfEmpty(ServerResponse.notFound().build());
}
Mono<ServerResponse> create(ServerRequest req) {
return req.bodyToMono(Person.class)
.flatMap(service::save)
.flatMap(saved -> ServerResponse.created(req.uriBuilder().path("/{id}").build(saved.id()))
.bodyValue(saved));
}
}go deeper
Can register a single @Bean RouterFunction and inline a handler.
Applies the router/handler split, uses method references, injects services into the handler component.
Reasons about multiple beans, ordering, reactive error handling, and testability tradeoffs.
Sets team conventions for structuring functional endpoints and integrating with the service/domain layers.
**Why split router from handler?** Putting routing and business logic in the same lambda quickly becomes unreadable and untestable. The community-standard pattern mirrors 'controller vs service': a thin **router configuration** and a **handler component**. **1. The router bean.** A `@Configuration` class declares a method annotated `@Bean` returning `RouterFunction<ServerResponse>`. It receives the handler as a parameter (Spring injects it) and only wires predicates to handler methods via method references: ```java @Bean RouterFunction<ServerResponse> personRoutes(PersonHandler h) { return route() .GET("/persons/{id}", h::getById) .GET("/persons", h::list) .POST("/persons", h::create) .build(); } ``` **2. The handler component.** A `@Component` (often named `...Handler`) holds the methods. Each has the `HandlerFunction` shape `Mono<ServerResponse> m(ServerRequest)` and depends on services via constructor injection: ```java @Component class PersonHandler { private final PersonService service; PersonHandler(PersonService service) { this.service = service; } Mono<ServerResponse> getById(ServerRequest req) { return service.find(req.pathVariable("id")) .flatMap(p -> ServerResponse.ok().bodyValue(p)) .switchIfEmpty(ServerResponse.notFound().build()); } } ``` **3. Automatic discovery.** You never call anything to 'install' the routes. `RouterFunctionMapping`, auto-configured by Spring Boot's WebFlux support, scans the `ApplicationContext` for every bean of type `RouterFunction<ServerResponse>`, combines them (in bean order), and inserts them into the `DispatcherHandler` dispatch chain. Adding a new feature = adding another `@Bean RouterFunction<ServerResponse>`; no central registry to edit. **4. Reading and writing.** `ServerRequest` gives functional access: `pathVariable`, `queryParam`, `headers()`, and reactive body access `bodyToMono(Type.class)` / `bodyToFlux(Type.class)`. `ServerResponse` is a fluent builder terminating in `Mono<ServerResponse>`: `ok().bodyValue(obj)` for a materialized value, `.body(publisher, Type.class)` to stream a reactive source, `status(...).build()` for empty bodies, plus `.contentType(...)`, `.header(...)`, `.cookie(...)`. **5. Handling 'not found' / empty.** A common idiom is `switchIfEmpty(ServerResponse.notFound().build())` on a `Mono` that may complete empty, and `onErrorResume` for error mapping — since there is no `@ExceptionHandler` in the functional model (see filters/`onError` for cross-cutting error handling). **Gotchas:** - The handler methods must be reachable as method references with the exact `ServerRequest -> Mono<ServerResponse>` signature. - Don't inject blocking repositories and block inside a handler — stay reactive (`flatMap`, not `.block()`). - Multiple beans work, but if two routes match the same request, the one whose bean is combined first wins — control it with `@Order` on the beans or careful predicate design. - The handler doesn't need `@Component` if you instantiate it directly in the config method, but a bean makes it independently testable and injectable. **When to use:** any non-trivial functional-endpoint app. The split is essentially mandatory for maintainability once you pass a couple of routes.
- Do you have to manually register the RouterFunction bean anywhere?No. RouterFunctionMapping automatically discovers all RouterFunction<ServerResponse> beans in the context and wires them into dispatch.
- How would you test these routes without a running server?Use WebTestClient.bindToRouterFunction(router).build(), which exercises the RouterFunction directly in-memory.
saying these in an interview costs you the question
- Calling .block() inside a handler instead of staying reactive.
- Putting all business logic in the router lambdas.
- Thinking you must @Autowire or list the RouterFunction in some registry.
- Expecting @ExceptionHandler to work in the functional model.