Master Spring Boot 3 Functional Routing: Handlers, Reactive APIs, and Advanced Techniques

This article explains how to replace annotation‑based controllers with Spring Boot 3 functional routing, covering both servlet‑blocking and reactive WebFlux implementations, complete with code examples for handlers, routers, parameter validation, nested routes, filters, and resource redirects.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Spring Boot 3 Functional Routing: Handlers, Reactive APIs, and Advanced Techniques

Introduction

Spring Boot 3 provides a functional routing model that replaces annotation‑based controllers ( @RestController, @RequestMapping) with plain Java functions. The model is built around HandlerFunction (receives a ServerRequest and returns a ServerResponse) and RouterFunction (maps request predicates to handler functions). It works for both servlet‑blocking and reactive (WebFlux) stacks, reducing boilerplate and improving readability.

Core Concepts

HandlerFunction

is a functional interface equivalent to the body of an @RequestMapping method. RouterFunction is created via RouterFunctions.route() (or RouterFunctions.nest()) and binds HTTP predicates (path, method, media type) to handler functions.

Practical Cases

2.1 Servlet‑Blocking Router Interface

Required static imports:

import static org.springframework.web.servlet.function.RouterFunctions.*;
import static org.springframework.http.MediaType.*;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.ServerResponse.*;

Handler implementation (business logic):

@Component
public class PersonHandler {
    private final PersonRepository personRepository;
    public PersonHandler(PersonRepository personRepository) {
        this.personRepository = personRepository;
    }
    public ServerResponse listPeople(ServerRequest request) {
        List<Person> people = personRepository.findAll();
        return ok().contentType(APPLICATION_JSON).body(people);
    }
    public ServerResponse createPerson(ServerRequest request) {
        Person person = request.body(Person.class);
        personRepository.save(person);
        return ok().build();
    }
    public ServerResponse getPerson(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return personRepository.findById(id)
                .map(p -> ok().contentType(APPLICATION_JSON).body(p))
                .orElseGet(() -> notFound().build());
    }
}

Router bean linking the handler methods:

@Bean
public RouterFunction<ServerResponse> router(PersonHandler handler) {
    return route()
            .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
            .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
            .POST("/person", handler::createPerson)
            .build();
}

2.2 Reactive Router Interface (WebFlux)

Add the WebFlux starter:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Reactive handler using Mono:

@Component
public class UserHandler {
    public Mono<ServerResponse> getById(ServerRequest request) {
        String id = request.pathVariable("id");
        return ServerResponse.ok().bodyValue("Received ID: " + id);
    }
    public Mono<ServerResponse> getByParam(ServerRequest request) {
        String name = request.queryParam("name").orElse("Guest");
        return ServerResponse.ok().bodyValue("Hello, " + name);
    }
    public Mono<ServerResponse> createUser(ServerRequest request) {
        return request.bodyToMono(User.class)
                .flatMap(user -> ServerResponse.ok().bodyValue("User created: " + user.getName()));
    }
}

Router definition for the reactive handlers:

@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
    return route()
            .GET("/", handler::index)
            .GET("/{id}", handler::getById)
            .GET("/param", handler::getByParam)
            .POST("/user", handler::createUser)
            .build();
}

2.3 Parameter Validation

Example using Spring’s Validator to validate a request body before processing:

@Component
public class PersonHandler {
    private final PersonRepository personRepository;
    private final Validator validator;
    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> personMono = request.bodyToMono(Person.class)
                .doOnNext(this::validate);
        return ok().build(personRepository.savePerson(personMono));
    }
    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString());
        }
    }
}

2.4 Nested Routes

Group routes under a common path using path:

RouterFunction<ServerResponse> route = route()
        .path("/person", builder -> builder
                .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
                .GET(accept(APPLICATION_JSON), handler::listPeople)
                .POST(handler::createPerson))
        .build();

2.5 Route Filters

Apply pre‑processing or post‑processing logic with before, after or filter. Example adds a custom header before handling and logs the response after:

RouterFunction<ServerResponse> route = route()
        .path("/person", b1 -> b1
                .nest(accept(APPLICATION_JSON), b2 -> b2
                        .GET("/{id}", handler::getPerson)
                        .GET(handler::listPeople)
                        .before(request -> ServerRequest.from(request)
                                .header("X-RequestHeader", "Value")
                                .build()))
                .POST(handler::createPerson))
        .after((request, response) -> {
            // Example logging
            System.out.println("Response status: " + response.statusCode());
            return response;
        })
        .build();

2.6 Redirect to Resources (SPA Support)

Serve a static index page for non‑API, non‑error, non‑static‑resource requests (useful for single‑page applications):

ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js","css","ico","png","jpg","gif");
RequestPredicate spaPredicate = path("/api/**")
        .or(path("/error"))
        .or(pathExtension(extensions::contains))
        .negate();
RouterFunction<ServerResponse> redirectToIndex = route()
        .resource(spaPredicate, index)
        .build();

Conclusion

The functional routing model in Spring Boot 3 lets developers define request handling with plain Java functions, eliminating the need for repetitive annotations. Whether using the servlet stack or the reactive WebFlux stack, the same concepts— HandlerFunction, RouterFunction, validation, nesting, filtering, and resource redirection—apply, enabling concise, maintainable, and flexible web services.

Spring BootWebFluxServletFunctional RoutingHandlerFunctionRouterFunction
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.