Last week, I wrote on how to migrate an existing Spring Boot application with a functional approach toward configuration. Since then, I had a presentation on that subject at Rockstar Night in Kiev and I had interesting feedback.
Handler class is necessary
Handler functions cannot be moved to the package.
They need to conform to the ServerRequest → Mono<ServerResponse>
signature.
class PersonHandler(private val personRepository: PersonRepository) {
fun readAll(request: ServerRequest) =
ServerResponse.ok().body(personRepository.findAll())
fun readOne(request: ServerRequest) =
ServerResponse.ok().body(
personRepository.findById(request.pathVariable("id").toLong()))
}
If dependencies are required - and this is the case, they need to be provided in a wider scope. Classes make such a scope readily available.
Wrapper class around routes is not necessary
This is how routes could be written:
class PersonRoutes(private val handler: PersonHandler) {
fun routes() = router {
"/person".nest {
GET("/{id}", handler::readOne)
GET("/", handler::readAll)
}
}
}
And this is how they can be configured accordingly:
beans {
bean {
PersonRoutes(PersonHandler(ref())).routes()
}
}
Because routes can be directly injected with the handler
dependency, the wrapping class is not necessary.
The above code can be re-written as:
fun routes(handler: PersonHandler) = router {
"/person".nest {
GET("/{id}", handler::readOne)
GET("/", handler::readAll)
}
}
beans {
bean {
routes(PersonHandler(ref()))
}
}
Keeping the class is just an old reflex from a pre-functional world.
Organizing routes
The demo project only configures 2 routes, but in a real-world project, they are bound to be many more. This is going to become unmanageable at some point.
In the annotation world, paths are organized into controller classes. What could be the organization pattern for class-free route functions?
Routes can be easily composed with the andOther()
function.
It’s defined as such:
interface RouterFunction {
default RouterFunction<?> andOther(RouterFunction<?> other) {
return new RouterFunctions.DifferentComposedRouterFunction(this, other);
}
...
}
Let’s define different route functions:
fun routeId(handler: PersonHandler) = router {
GET("/person/{id}", handler::readOne)
}
fun routeAll(handler: PersonHandler) = router {
GET("/person", handler::readAll)
}
Composing them is now quite straightforward:
bean {
val handler = PersonHandler(ref())
routeId(handler).andOther(routeAll(handler))
}
Or with stdlib:
bean {
with(PersonHandler(ref())) {
routeId(this).andOther(routeAll(this))
}
}