1. Introduction
Hello friend! 🙂 Welcome to my next practical tutorial, in which I will show you how to expose web endpoints using Spring Boot 3 and Kotlin router DSL (aka- functional style).
Basically, the Kotlin router DSL comes in 3 variants:
- WebMvc.fn DSL using
router { }
– which we can use when working with a “standard” MVC Spring Project, - WebFlux.fn Reactive DSL with
router { }
– used when working with a reactive-stack framework- Spring Webflux, - WebFlux.fn Coroutines DSL with
coRouter { }
– as the name suggests- the one we will use when working with coroutines.
And although in this article, we will make use of the coroutine-based REST API implemented in “Reactive REST API With Spring, Kotlin, and Coroutines“, the knowledge you’re gonna gain today is universal and will work regardless of the implementation.
Video Tutorial
If you prefer video content, then check out my video:
If you find this content useful, please leave a subscription 😉
2. Project Configuration
As I mentioned in the introduction, we will reuse the already implemented API and rewrite the @RestController approach using the Spring Boot Kotlin DSL router.
Make a real progress thanks to practical examples, exercises, and quizzes.
- 64 written lessons
- 62 quizzes with a total of 269 questions
- Dedicated Facebook Support Group
- 30-DAYS Refund Guarantee
So, as the first step, let’s clone this GitHub repository.
When we navigate to the controller
package, we will see that the project contains 3 controllers:
- UserController
- SearchController
- CompanyController
And for the purpose of this tutorial, we will focus on the API responsible for users management:
@RestController @RequestMapping("/api/users") class UserController( private val userService: UserService ) { @PostMapping suspend fun createUser(@RequestBody userRequest: UserRequest): UserResponse = userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during user creation.") @GetMapping suspend fun findUsers( @RequestParam("name", required = false) name: String? ): Flow { val users = name?.let { userService.findAllUsersByNameLike(name) } ?: userService.findAllUsers() return users.map(User::toResponse) } @GetMapping("/{id}") suspend fun findUserById( @PathVariable id: Long ): UserResponse = userService.findUserById(id) ?.let(User::toResponse) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") @DeleteMapping("/{id}") suspend fun deleteUserById( @PathVariable id: Long ) { userService.deleteUserById(id) } @PutMapping("/{id}") suspend fun updateUser( @PathVariable id: Long, @RequestBody userRequest: UserRequest ): UserResponse = userService.updateUser( id = id, requestedUser = userRequest.toModel() ) .toResponse() } private fun UserRequest.toModel(): User = User( email = this.email, name = this.name, age = this.age, companyId = this.companyId ) fun User.toResponse(): UserResponse = UserResponse( id = this.id!!, email = this.email, name = this.name, age = this.age )
3. Convert UserController To Handler
Before we will start working with Spring Boot Kotlin DSL, let’s make a few adjustments.
Firstly, let’s add a new package called handler
.
Then, let’s remove all annotations inside the class and mark the UserHandler with @Component:
@Component class UserHandler( private val userService: UserService ) { suspend fun createUser(userRequest: UserRequest): UserResponse = userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during user creation.") suspend fun findUsers( name: String? ): Flow { val users = name?.let { userService.findAllUsersByNameLike(name) } ?: userService.findAllUsers() return users.map(User::toResponse) } suspend fun findUserById( id: Long ): UserResponse = userService.findUserById(id) ?.let(User::toResponse) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") suspend fun deleteUserById( id: Long ) { userService.deleteUserById(id) } suspend fun updateUser( id: Long, userRequest: UserRequest ): UserResponse = userService.updateUser( id = id, requestedUser = userRequest.toModel() ) .toResponse() }
4. Implement Coroutines router with Kotlin DSL
With all of that being done, let’s create a new class Config inside the config
package:
@Configuration class Config { @Bean fun router( userHandler: UserHandler ) = coRouter { accept(MediaType.APPLICATION_JSON).nest { "/api/users".nest { POST("", userHandler::createUser) GET("", userHandler::findUsers) GET("/{id}", userHandler::findUserById) DELETE("/{id}", userHandler::deleteUserById) PUT("/{id}", userHandler::updateUser) } } } }
As I mentioned in the beginning when working with coroutines, we will create a RouterFunction using coRouter{ }. Nevertheless, when working with MVC or Spring WebFlux, then the choice would be the router {}.
As we can see, the nest {} function allows us to structure routing in a neat and readable manner.
Let’s take a look at the following example:
"/api/v1".nest { "/whatever".nest { "/sub-whatever".nest { // whatever } } } "/api/v2".nest { // another handlers }
This way, we can manage versioning in one place.
Nevertheless, let’s get back to our user handlers:
POST("", userHandler::createUser) GET("", userHandler::findUsers) GET("/{id}", userHandler::findUserById) DELETE("/{id}", userHandler::deleteUserById) PUT("/{id}", userHandler::updateUser)
At this point, our application won’t compile, because of one, important thing- handler functions must take only one parameter of the ServerRequest type.
So in order to fix the app, we will have to update our Spring controller for functional routing.
5. Update POST Endpoint
So as the first step, let’s make the necessary adjustments to the createUser
:
suspend fun createUser(request: ServerRequest): ServerResponse { val userRequest = request.awaitBody(UserRequest::class) val savedUserResponse = userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during user creation.") return ServerResponse.ok() .bodyValueAndAwait(savedUserResponse) } // Before: @PostMapping suspend fun createUser(@RequestBody userRequest: UserRequest): UserResponse = userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error during user creation.")
As we can see, instead of binding the JSON payload to the UserRequest instance with an annotation, we have to do it manually with .awaitBody()
.
Whatsoever, the return type changes to ServerResponse, which we have to create manually.
And although the final choice is up to you, instead of throwing the ResponseStatusException, we can compose the HTTP error response on our own:
suspend fun createUser(request: ServerRequest): ServerResponse { val userRequest = request.awaitBody(UserRequest::class) return userService.saveUser( user = userRequest.toModel() ) ?.toResponse() ?.let { response -> ServerResponse.ok() .bodyValueAndAwait(response) } ?: ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) .buildAndAwait() }
6. Migrate GET All Users
Nextly, let’s migrate the findUsers()
function and learn how to read query parameters:
suspend fun findUsers( request: ServerRequest ): ServerResponse { val users = request.queryParamOrNull("name") ?.let { name -> userService.findAllUsersByNameLike(name) } ?: userService.findAllUsers() val usersResponse = users.map(User::toResponse) return ServerResponse.ok() .bodyAndAwait(usersResponse) } // Before: @GetMapping suspend fun findUsers( @RequestParam("name", required = false) name: String? ): Flow { val users = name?.let { userService.findAllUsersByNameLike(name) } ?: userService.findAllUsers() return users.map(User::toResponse) }
This time, in order to read a particular query parameter, we can use the queryParamOrNull(String)
. However, the ServerRequest interface comes with two, additional functions, which we could use here:
queryParam(String)
, which returns the Optional,- and
queryParams()
returning an instance of MultiValueMap<String, String> with all parameters.
And just, like previously we compose a ServerResponse instance, but this time using the bodyAndAwait()
, which takes a Flow as an argument.
7. GET User By Id
As the next example in our Spring Boot Kotlin DSL project, let’s rewrite the findUserById()
and see how we can read a path variable:
suspend fun findUserById( request: ServerRequest ): ServerResponse { val id = request.pathVariable("id").toLong() val userResponse = userService.findUserById(id) ?.let(User::toResponse) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.") return ServerResponse.ok() .bodyValueAndAwait(userResponse) } // Before: @GetMapping("/{id}") suspend fun findUserById( @PathVariable id: Long ): UserResponse = userService.findUserById(id) ?.let(User::toResponse) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "User with id $id was not found.")
As we can see, in order to read the path variable, we have to use the pathVariable(String)
(or alternatively, pathVariables()
). Unfortunately, this function cannot be parametrized and we have to take care of Long conversions manually.
8. DELETE Endpoint
Following, let’s update the deleteUserById()
:
suspend fun deleteUserById( request: ServerRequest ): ServerResponse { val id = request.pathVariable("id").toLong() userService.deleteUserById(id) return ServerResponse.noContent() .buildAndAwait() } // Before: @DeleteMapping("/{id}") suspend fun deleteUserById( @PathVariable id: Long ) { userService.deleteUserById(id) }
And this time, instead of returning anything in the response payload, we make use of the noContent()
along with buildAndAwait()
to return a bodiless entity.
9. User Update With PUT
Lastly, let’s make the necessary adjustments to the updateUser()
:
suspend fun updateUser( request: ServerRequest ): ServerResponse { val id = request.pathVariable("id").toLong() val userRequest = request.awaitBody(UserRequest::class) val userResponse = userService.updateUser( id = id, requestedUser = userRequest.toModel() ).toResponse() return ServerResponse.ok() .bodyValueAndAwait(userResponse) } // Before: @PutMapping("/{id}") suspend fun updateUser( @PathVariable id: Long, @RequestBody userRequest: UserRequest ): UserResponse = userService.updateUser( id = id, requestedUser = userRequest.toModel() ) .toResponse()
As we can see, nothing new shows up in this example.
10. Testing And Homework
With all of that being done, we can finally run our application and verify whether handlers are working, as expected.
Your homework will be to:
- Test whether the routing works fine after we converter our Spring controller to the functional style.
- Refactor the CompanyController and SearchController classes in a similar manner.
As a bonus, right here you can find a Postman collection, which you can use for testing.
Note: If you would like to see the solution, then check out the router-dsl branch in my GitHub repo.
11. Spring Boot 3 With Kotlin DSL Summary
And that’s all for this tutorial, in which we have learned how to expose web endpoints using Spring Boot 3 and Kotlin router DSL (aka- functional style).
Hope you enjoyed this content, and if so, then please reach out in the comments section.
Have a great week!