REST Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

A lot of frameworks and projects are introducing reactive programming and asynchronous request handling. Consequently, Spring 5 introduced a reactive WebClient implementation as a part of the WebFlux framework.

In this tutorial, we'll see how to reactively consume REST API endpoints with WebClient.

2. REST API Endpoints

To start with, let's define a sample REST API with the following GET endpoints:

  • /products – get all products
  • /products/{id} – get product by ID
  • /products/{id}/attributes/{attributeId} – get product attribute by id
  • /products/?name={name}&deliveryDate={deliveryDate}&color={color} – find products
  • /products/?tag[]={tag1}&tag[]={tag2} – get products by tags
  • /products/?category={category1}&category={category2} – get products by categories

So, we've just defined a few different URIs. In just a moment, we'll figure out how to build and send each type of URI with WebClient.

Please note the URIs for gettings products by tags and by categories contain arrays as query parameters. However, the syntax differs. Since there is no strict definition of how arrays should be represented in URIs. Primarily, this depends on the server-side implementation. Accordingly, we'll cover both cases.

3. WebClient Setup

At first, we need to create an instance of WebClient. For this article, we'll be using a mocked object as far as we need just to verify that a valid URI is requested.

Let's define the client and related mock objects:

this.exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(this.exchangeFunction.exchange(this.argumentCaptor.capture())).thenReturn(Mono.just(mockResponse));
this.webClient = WebClient
  .builder()
  .baseUrl("https://example.com/api")
  .exchangeFunction(exchangeFunction)
  .build();

In addition, we've passed a base URL that will be prepended to all requests made by the client.

Lastly, to verify that a particular URI has been passed to the underlying ExchangeFunction instance, let's use the following helper method:

private void verifyCalledUrl(String relativeUrl) {
    ClientRequest request = this.argumentCaptor.getValue();
    Assert.assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
    Mockito.verify(this.exchangeFunction).exchange(request);
    verifyNoMoreInteractions(this.exchangeFunction);
}

The WebClientBuilder class has the uri() method that provides the UriBuilder instance as an argument. Generally, an API call is usually made in the following manner:

this.webClient.get()
  .uri(uriBuilder -> uriBuilder
    //... building a URI
    .build())
  .retrieve();

We'll use UriBuilder extensively in this guide to construct URIs. It's worth noting that we can build a URI using any other way and then just pass the generated URI as String.

4. URI Path Component

A path component consists of a sequence of path segments separated by a slash ( / ). First, let's start with a simple case when a URI doesn't have any variable segments /products:

this.webClient.get()
  .uri("/products")
  .retrieve();
verifyCalledUrl("/products");

For that case, we can just pass a String as an argument.

Next, let's take the /products/{id} endpoint and build the corresponding URI:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}")
    .build(2))
  .retrieve();
verifyCalledUrl("/products/2");

From the code above, we can see that actual segment values are passed to the build() method.
Now, in a similar way we can create a URI with multiple path segments for the /products/{id}/attributes/{attributeId} endpoint:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/{id}/attributes/{attributeId}")
    .build(2, 13))
  .retrieve();
verifyCalledUrl("/products/2/attributes/13");

A URI can have as many path segments as required. Of course, if the final URI length is not exceeding limitations. Lastly, remember to keep the right order of actual segment values passed to the build() method.

5. URI Query Parameters

Usually, a query parameter is a simple key-value pair like title=Baeldung. Let's see how to build such URIs.

5.1. Single Value Parameters

Let's start with single value parameters and take the /products/?name={name}&deliveryDate={deliveryDate}&color={color} endpoint. To set a query parameter we call the queryParam() method of the UriBuilder interface:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

Here we've added three query parameters and assigned actual values immediately. Additionally, it's also possible to leave placeholders instead of exact values:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "{title}")
    .queryParam("color", "{authorId}")
    .queryParam("deliveryDate", "{date}")
    .build("AndroidPhone", "black", "13/04/2019"))
  .retrieve();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");

Especially, this might be helpful when passing a builder object further in a chain. Please note one important difference between the two code snippets above.

With attention to the expected URIs, we can see that they were encoded differently. Particularly, the slash character ( / ) was escaped in the last example. Generally speaking, RFC3986 doesn't require encoding of slashes in the query.

However, some server-side applications might require such conversion. Therefore, we'll see how to change this behavior later in this guide.

5.2. Array Parameters

Likewise, we may need to pass an array of values. Still, there are no strict rules for passing arrays in a query string. Therefore, an array representation in a query string differs from project to project and usually depends on underlying frameworks. We'll cover the most widely used formats.

Let's start with the /products/?tag[]={tag1}&tag[]={tag2} endpoint:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("tag[]", "Snapdragon", "NFC")
    .build())
  .retrieve();
verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");

As we can see, the final URI contains multiple tag parameters followed by encoded square brackets. The queryParam() method accepts variable arguments as values, so there is no need to call the method several times.

Alternatively, we can omit square brackets and just pass multiple query parameters with the same key, but different values – /products/?category={category1}&category={category2}:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", "Phones", "Tablets")
    .build())
  .retrieve();
verifyCalledUrl("/products/?category=Phones&category=Tablets");

To conclude, there is one more extensively-used method to encode an array is to pass comma-separated values. Let's transform our previous example into comma-separated values:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("category", String.join(",", "Phones", "Tablets"))
    .build())
  .retrieve();
verifyCalledUrl("/products/?category=Phones,Tablets");

Thus, we are just using the join() method of the String class to create a comma-separated string.  Sure, we can use any other delimiter that is expected by the application.

6. Encoding Mode

Remember how we mentioned URL encoding earlier.

If the default behavior doesn't fit our requirements, we can change it. We need to provide a UriBuilderFactory implementation while building a WebClient instance. In this case, we'll use the DefaultUriBuilderFactory class. To set encoding call the setEncodingMode() method. The following modes are available:

  • TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
  • VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
  • URI_COMPONENTS: Encode URI component value after expending URI variables
  • NONE: No encoding will be applied

The default value is TEMPLATE_AND_VALUES. Let's set the mode to URI_COMPONENTS:

DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
this.webClient = WebClient
  .builder()
  .uriBuilderFactory(factory)
  .baseUrl(BASE_URL)
  .exchangeFunction(exchangeFunction)
  .build();

As a result, the following assertion will succeed:

this.webClient.get()
  .uri(uriBuilder - > uriBuilder
    .path("/products/")
    .queryParam("name", "AndroidPhone")
    .queryParam("color", "black")
    .queryParam("deliveryDate", "13/04/2019")
    .build())
  .retrieve();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");

And, of course, we can provide a completely custom UriBuilderFactory implementation to handle URI creation manually.

7. Conclusion

In this tutorial, we’ve seen how to build different types of URIs using WebClient and DefaultUriBuilder.

Along the way, we've covered various types and formats of query parameters. And we wrapped up with changing the default encoding mode of the URL builder.

All of the code snippets from the article, as always, are available over on GitHub repository.

REST bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!