Spring WebClient With Kotlin Coroutines

The image is a featured image for article titled " Spring WebClient With Kotlin Coroutines" and consist of Spring WebFlux logo in the foreground and a blurred photo of a PC in the background.

1. Introduction

Welcome to my next article, in which I will show you everything you need to know when working with Spring WebClient and Kotlin coroutines.

To be on the same page- this article will be a coroutine-focused extension of my other blog post about Spring 5 WebClient with Spring Boot. So, instead of duplicating the content, which works exactly the same regardless of whether we pick the Reactor or coroutines implementation, I will focus on response handling. Nevertheless, if you are interested in other features, like request bodies, headers, and filter implementation, then I recommend checking out that article, as well.

With that being said, let’s get to work 🙂

Video Tutorial

If you prefer video content, then check out my video:

If you find this content useful, please leave a subscription  😉

2. API Specification

Before I show you how to implement WebClient with Kotlin coroutines, let me quickly describe the API we are about to query.

Basically, we will be using 3 endpoints in order to:

  • fetch a list of all users,
  • get user by its identifier,
  • and delete a particular user by id.

And to better visualize, let’s take a look at the API “contract”:

GET http://localhost:8090/users 

# Example response:
[
  {
    "id": 1,
    "first_name": "Robert",
    "last_name": "Smith"
  },
  {
    "id": 2,
    "first_name": "Mary",
    "last_name": "Jones"
  }
...
]

GET http://localhost:8090/users/1

# Example successful response body:
{
  "id": 1,
  "first_name": "Robert",
  "last_name": "Smith"
}

# If the user is not found, then the API returns 404 NOT FOUND

DELETE http://localhost:8090/users/1

# Responds 204 No Content with empty body

Make a real progress thanks to practical examples, exercises, and quizzes.

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

3. Configure Spring WebClient

And although the Spring WebClient configuration does not differ when working with coroutines, let’s take a look at the config class:

@Configuration
class Config {

  @Bean
  fun webClient(builder: WebClient.Builder): WebClient =
      builder
        .baseUrl("http://localhost:8090")
        .build()
}

As we can see, with this config we define a new WebClient bean and instruct that a base URL for our requests is http://localhost:8090.

4. Implement UserResponse

Nextly, let’s add the UserResponse class to our codebase:

data class UserResponse(
  val id: Long,
  @JsonProperty("first_name") val firstName: String,
  @JsonProperty("last_name") val lastName: String
)

And this class will be responsible for serializing responses from the external API.

5. Spring WebClient With Kotlin Coroutines

As the next step, we can finally implement the UserApiService and start querying.

Of course, let’s inject the WebClient before heading to the next steps:

@Service
class UserApiService(
  private val webClient: WebClient
) {

}

5.1 Obtaining Response Body With awaitBody()

Firstly, let’s learn how to fetch a list of users:

suspend fun findAllUsers(): List<UserResponse> =
  webClient.get()
    .uri("/users")
    .retrieve()
    .awaitBody<List<UserResponse>>()

Well, we have to remember that the awaitBody() is a suspend function- and that’s the reason why our function is defined as a suspend, as well. Moreover, we have to define the type of response we are expecting (List<UserResponse> in our case). Of course, it’s optional if we already defined a return type explicitly for our function.

5.2 awaitBodyOrNull()

Additionally, we can make use of another variant, awaitBodyOrNull():

suspend fun findUserById(id: Long): UserResponse? =
  webClient.get()
    .uri("/users/$id")
    .retrieve()
    .awaitBodyOrNull<UserResponse>()

This time, if the Mono completes without a value, a null is returned.

Nevertheless, please keep in mind that by default when using retrieve() 4xx and 5xx responses result in a WebClientResponseException. Later, I will show you how to customize this behavior using onStatus().

5.3 Return User List As A Flow

As the next step, let’s learn how to transform our publisher into Flow:

fun findAllUsersFlow(): Flow<UserResponse> =
  webClient.get()
    .uri("/users")
    .retrieve()
    .bodyToFlow<UserResponse>()

This time, instead of converting users’ responses into a List, we invoke the .bodyToFlow<UserResponse>(), which underneath transforms a Flux<UserResponse> into the Flow.

5.4 WebClient retrieve vs exchange

Well, although I promised not to cover things twice, I believe it’s important to say a couple of words here.

The main difference between retrieve() and exchange() methods is that the exchange() returns additional HTTP information, like headers and status. Nevertheless, when using it, it is our responsibility to handle all response cases to avoid memory leaks! And because of that, the exchange() was deprecated in Spring 5.3 and we should rather use exchangeToMono(Function) and exchangeToFlux(Function)(of course, if the retrieve() is not sufficient for our needs).

When working with Spring WebClient and Kotlin coroutines, we can make use of the awaitExchange and exchangeToFlow functions.

5.5 awaitExchange()

With that being said, let’s implement another function using awaitExchange:

suspend fun findAllUsersUsingExchange(): List<UserResponse> =
  webClient.get()
    .uri("/users")
    .awaitExchange { clientResponse ->
      val headers = clientResponse.headers().asHttpHeaders()
      logger.info("Received response from users API. Headers: $headers")
      clientResponse.awaitBody<List<UserResponse>>()
    }

As we can see, this function can be a good choice if we would like to access additional response information (like headers in our case) and perform additional logic based on them.

5.6 exchangeToFlow()

On the other hand, if we would like to return the Flow, then we must choose exchangeToFlow variant:

fun findAllUsersFlowUsingExchange(): Flow<UserResponse> =
  webClient.get()
    .uri("/users")
    .exchangeToFlow { clientResponse ->
      val headers = clientResponse.headers().asHttpHeaders()
      logger.info("Received response from users API. Headers: $headers")
      clientResponse.bodyToFlow<UserResponse>()
    }

5.7 Work With Bodiless

Sometimes, the REST API endpoints do not return any response body, which is usually indicated by the 204 No Content status code.

In such a case we can either choose awaitBody and pass the Unit type, or awaitBodilessEntity:

suspend fun deleteUserById(id: Long): Unit =
  webClient.delete()
    .uri("/users/$id")
    .retrieve()
    .awaitBody<Unit>()

suspend fun deleteUserById(id: Long): ResponseEntity<Void> =
  webClient.delete()
    .uri("/users/$id")
    .retrieve()
    .awaitBodilessEntity()

As we can see, depending on our needs we can either simply return the Unit or the ResponseEntity instance.

5.8 Handling Errors

And although the last example does not differ when working with WebClient and Kotlin coroutines, I feel obliged to show how we can handle API error status codes.

As I mentioned previously, by default all 4xx and 5xx response status codes cause WebClientResponseException to be thrown. And if we would like to alter this behavior, Spring WebFlux comes with a useful onStatus method for that:

suspend fun findUserByIdNotFoundHandling(id: Long): UserResponse =
  webClient.get()
    .uri("/users/$id")
    .retrieve()
    .onStatus({ responseStatus ->
      responseStatus == HttpStatus.NOT_FOUND
    }) { throw ResponseStatusException(HttpStatus.NOT_FOUND) }
    .awaitBody<UserResponse>()

The onStatus takes two parameters: the predicate and a function. In our example, each 404 Not Found response from the external API will be translated to ResponseStatusException with the same HttpStatus.

6. Spring WebClient With Kotlin Coroutines Summary

And that’s all for this article. Together, we’ve learned how easily we can implement a Spring WebClient with Kotlin coroutines using built-in features.

As always, you can find the example source code in this GitHub repository and I will be thankful if you would like to share some feedback with me right here, in the comments section.

Share this:

Hi there! 👋

Hi there! 👋

My name is Piotr and I've created Codersee to share my knowledge about Kotlin, Spring Framework, and other related topics through practical, step-by-step guides. Always eager to chat and exchange knowledge.

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

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.