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.
- 64 written lessons
- 62 quizzes with a total of 269 questions
- Dedicated Facebook Support Group
- 30-DAYS Refund Guarantee
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.