Spring Boot 3 Kotlin Router DSL

In this practical tutorial, I will show you how to expose web endpoints using Spring Boot 3 and Kotlin router DSL (functional style).
This image is a for an article titled " Spring Boot 3 Kotlin Router DSL" and consist of Spring Boot logo in the foreground and two people in the blurred background sitting and standing next to a desk.

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.

Image presents a Kotlin Course box mockup for "Kotlin Handbook. Learn Through Practice"

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:

  1. Test whether the routing works fine after we converter our Spring controller to the functional style.
  2. 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!

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy