Featured image for "Battle of the Spring REST clients: RestTemplate, WebClient or RestClient?"

Battle of the Spring REST clients: RestTemplate, WebClient or RestClient?

October 26th, 2023
15 minute read
Spring boot Spring

Intro

Recently, I’ve seen a few discussions where people were talking about whether they should use RestTemplate or WebClient. One of their arguments is that you should use WebClient because RestTemplate is deprecated (spoiler alert: it’s not!). In this blogpost I’ll talk about the options you have within the Spring framework to make REST calls.

Contents

  1. RestTemplate
    1. Setup
    2. Retrieving data
    3. Sending data
    4. Error handling
    5. Testing
    6. Pros
    7. Cons
  2. WebClient
    1. Setup
    2. Retrieving data
    3. Sending data
    4. Error handling
    5. Testing
    6. Pros
    7. Cons
  3. RestClient
    1. Setup
    2. Retrieving data
    3. Sending data
    4. Error handling
    5. Pros
    6. Cons
  4. Deprecation?
  5. Conclusion

RestTemplate

Setup

RestTemplate is the true OG. It has been available since Spring 3.0 (that’s from before Spring Boot was born) and supports synchronous HTTP requests. This client has several methods available, such as getForObject(), getForEntity(), exchange(), … .

The easiest way to construct a RestTemplate is by creating a bean based on the RestTemplateBuilder. For example:

@Bean
public RestTemplate dummyAPIRestTemplate(RestTemplateBuilder builder) {
    return builder.rootUri("https://dummyjson.com").build();
}

This setup allows you to add additional interceptors, configure a root URL, … .

Retrieving data

After that, you can autowire the RestTemplate bean and call one of its many methods. For example:

Product product = restTemplate.getForObject("/products/1", Product.class);
logger.info("Found product: {}", product);

Alternatively, you can also use URL variables:

Product product = restTemplate.getForObject("/products/{id}", Product.class, 1);
logger.info("Found product: {}", product);

In the example above, {id} is a URL variable. Spring will then replace them with the provided URL variables (1) in the same order as they’re in the URL.

If you prefer using the names, you can also provide a Map:

Map<String, Object> variables = Map.of("id", 1);
Product product = restTemplate.getForObject("/products/{id}", Product.class, variables);
logger.info("Found product: {}", product);

If you’re interested in the response headers, the getForObject() method won’t work. In that case, you can use the getForEntity() method. This method returns a ResponseEntity wrapper which incldudes both the headers and the body. For example:

Map<String, Object> variables = Map.of("id", 1);
ResponseEntity<Product> entity = restTemplate.getForEntity("/products/{id}", Product.class, variables);
logger.info("Found product: {}", entity.getBody());
logger.info("Found headers: {}", entity.getHeaders());

Sending data

If you want to send data, you can use the postForObject() method. For example:

CreateProductRequest request = new CreateProductRequest("Banana");
Product product = restTemplate.postForObject("/products/add", request, Product.class);
logger.info("Created product: {}", product);

In this example, CreateProductRequest is a class that follows the JSON specification of the request body of the DummyJSON API.

For other HTTP methods such as PUT, there isn’t a putForObject() method. In that case, you have to rely on the exchange() method. For example:

UpdateProductRequest request = new UpdateProductRequest("Bananaaaaa");
HttpEntity<UpdateProductRequest> httpEntity = new HttpEntity<>(request, null);
RequestEntity<Product> entity = restTemplate.exchange("/products/101", HttpMethod.PUT, httpEntity, Product.class);
logger.info("Updated product: {}", entity.getBody());

With the exchange() method, you always get a ResponseEntity like in the postForEntity() method. In addition, rather than passing the request body immediately, you have to provide an HttpEntity. This class accepts both a request body and request headers. In this example, I’m passing null as the headers.

Error handling

To handle errors, you can catch RestClientResponseException for all errors, or one of its implementations such as HttpClientErrorException to only handle certain errors. For example:

try {
    Map<String, Object> variables = Map.of("id", -1);
    Product product = restTemplate.getForObject("/products/{id}", Product.class, variables);
    logger.info("Found product: {}", product);
} catch (HttpClientErrorException ex) {
    DummyAPIError error = ex.getResponseBodyAs(DummyAPIError.class);
    logger.error("Retrieving product failed: {}", error.message(), ex);
}

In this example, the code snippet will print Retrieving product failed: Product with id '-1' not found.

Testing

Testing with RestTemplate can be done with MockRestServiceServer. MockRestServiceServer intercepts call to the RestTemplate (so no actual network traffic happens), and comes with a whole assertion library.

To use this, annotate your test with @RestClientTest, autowire MockRestServiceServer and then you can write tests like this:

server
    .expect(once(), requestTo(startsWith("https://dummyjson.com/products/1")))
    .andExpect(method(HttpMethod.GET))
    .andRespond(withSuccess(new ClassPathResource("dummy-product.json"), MediaType.APPLICATION_JSON));

In this test I’m verifying that a request is made to /products/1, and I respond with a JSON that I put in a file called dummy-product.json. You can read more about this in my tutorial about testing your rest clients with Spring.

Pros

The main advantage of RestTemplate is that it has a rich ecosystem. There are integrations with various other parts of the Spring framework (like Spring Cloud), there is support for testing and so on.

Cons

One of the drawbacks of RestTemplate is that the API feels dated. In addition, due to it being based on HttpURLConnection, there is no support for PATCH-requests, though this can be added by using the HttpComponentsClientHttpRequestFactory. To use this, you need to add the Apache HttpComponents library.

The last drawback is that RestTemplate only supports synchronous, blocking requests. If you’re interested in using non-blocking communication, RestTemplate isn’t for you.

WebClient

Setup

Since Spring 5.0 and Spring Boot 2.0, reactive support was added to the framework. One of these classes is the WebClient, which allows you to perform HTTP requests in a reactive way. This means that WebClient is asynchronous and non-blocking by default.

To use it, you can create a WebClient bean using the builder:

@Bean
public WebClient dummyAPIWebClient(WebClient.Builder builder) {
    return builder
        .baseUrl("https://dummyjson.com")
        .build();
}

Retrieving data

To use the WebClient, you can use its fluent API. For example, to retrieve data, you can use:

Mono<Product> productMono = webClient
    .get()
    .uri(builder -> builder
        .path("/products/1")
        .build())
    .retrieve()
    .bodyToMono(Product.class);
productMono.subscribe(product -> logger.info("Found product: {}", product));

Important here to realize is that whatever the WebClient returns is a reactive Publisher. At the time you get the productMono, the HTTP request has not been executed yet!

To asynchronously execute whatever logic is within your reactive pipeline, you need to add a subscriber. This is done by adding the subscribe() statement. But be aware that even then, you can only access the return value from within the subscribe() callback.

You can use the block() method to immediately return a value. But using block() is not recommended because you will be going back to synchronous, blocking API calls. So, since you shouldn’t use block() and you can only retreive the data within the reactive pipeline, it means that you should write your code so that everything works with reactive elements.

One of the nice parts of this fluent API is that it also comes with a fluent API to build the URL. So far, we’ve only used the path(), but you can also use pathSegment(), queryParam() and so on.

You can also work with URL variables just like within RestTemplate. For example:

webClient
    .get()
    .uri(builder -> builder
        .pathSegment("products", "{id}") // Add a URL variable
        .build(Map.of("id", 1))) // Map each URL variable
    .retrieve()
    .bodyToMono(Product.class)
    .subscribe(product -> logger.info("Found product: {}", product));

Sending data

To send data, you can write the following code:

CreateProductRequest request = new CreateProductRequest("Banana");
webClient
    .post()
    .uri("/products/add")
    .bodyValue(request)
    .retrieve()
    .bodyToMono(Product.class)
    .subscribe(product -> logger.info("Created product: {}", product));

By using the bodyValue() method, we can pass a plain Java object. If the request body comes from another reactive stream, we can pass the Mono or Flux by using the body() method in stead. For example:

// I'm using Mono.just() to illustrate that this works
// Usually you won't wrap objects with Mono.just()
Mono<CreateProductRequest> requestMono = Mono.just(new CreateProductRequest("Banana"));
webClient
    .post()
    .uri("/products/add")
    .body(requestMono, CreateProductRequest.class)
    .retrieve()
    .bodyToMono(Product.class)
    .subscribe(product -> logger.info("Created product: {}", product));

The same can be applied to send a PUT request:

UpdateProductRequest request = new UpdateProductRequest("Bananaaaaa");
webClient
    .put()
    .uri("/products/101")
    .bodyValue(request)
    .retrieve()
    .bodyToMono(Product.class)
    .subscribe(product -> logger.info("Updated product: {}", product));

Error handling

When you subscribe to a reactive publisher, you can not only subscribe for values, but also subscribe to any error that might happen. By default, WebClient returns an error of type WebClientResponseException when it encounters any 4xx or 5xx status.

To listen to these, you can use:

webClient
    .get()
    .uri(builder -> builder
        .pathSegment("products", "{id}")
        .build(Map.of("id", -1)))
    .retrieve()
    .bodyToMono(Product.class)
    .subscribe(
        product -> logger.info("Found product: {}", product),
        ex -> logger.error("An error occured", ex));

Alternatively, if you just want to do a side effect for certain exception types, you can use the doOnError() handler:

webClient
    .get()
    .uri(builder -> builder
        .pathSegment("products", "{id}")
        .build(Map.of("id", -1)))
    .retrieve()
    .bodyToMono(Product.class)
    .doOnError(WebClientResponseException.class, ex -> {
        DummyAPIError error = ex.getResponseBodyAs(DummyAPIError.class);
        logger.error("Retrieving product failed: {}", error.message(), ex);
    })
    .subscribe(product -> logger.info("Found product: {}", product));

Within the doOnError() method, you can specify the exception type you want to listen for. This allows you to listen to exceptions such as WebClientResponseException. In addition, the WebClientResponseException has a getResponseBodyAs() method to deserialize the response body to a Java object.

If you want more control about which exception is thrown, you can also use the onStatus() method before you call the bodyToMono() function:

webClient
    .get()
    .uri(builder -> builder
        .pathSegment("products", "{id}")
        .build(Map.of("id", -1)))
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, response -> response
        .bodyToMono(DummyAPIError.class)
        .map(InvalidProductException::new)
        .flatMap(Mono::error))
    .bodyToMono(Product.class)
    .subscribe(product -> logger.info("Found product: {}", product));

This method comes with two arguments. The first argument is a predicate you can provide to filter out certain errors. In this example I’m only listening for 4xx errors.

The second argument returns the response, and expects a Mono.error() in return. To create a Mono.error(), you need to provide an exception, so I created an InvalidProductException that accepts a DummyAPIError as a parameter.

public class InvalidProductException extends RuntimeException {
    private final DummyAPIError error;

    public InvalidProductException(DummyAPIError error) {
        this.error = error;
    }

    @Override
    public String getMessage() {
        return this.error.message();
    }
}

If you want to apply this status handler to all requests, you can move this to the WebClient.Builder itself. For example:

builder
    .baseUrl("https://dummyjson.com")
    .defaultStatusHandler(HttpStatusCode::is4xxClientError, response -> response
        .bodyToMono(DummyAPIError.class)
        .map(InvalidProductException::new)
        .flatMap(Mono::error))
    .build();

Testing

Testing with WebClient is a bit more complex since there is no framework support such as MockRestServiceServer for RestTemplate. For testing, I usually use OkHttp’s MockWebServer.

A big difference between MockWebServer and MockRestServiceServer is that with MockWebServer you’re really setting up a webserver and sending HTTP calls. With MockRestServiceServer on the other hand, the calls are intercepted and never really sent out.

To use MockWebServer, you need to add an extra dependency, such as:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <scope>test</scope>
</dependency>

After that, I usually set up my tests like this:

@BeforeEach
void setUp() throws IOException {
    server = new MockWebServer();
    server.start();
    String rootUrl = server.url("/").toString();
    client = WebClient.builder().baseUrl(rootUrl).build();
}

@AfterEach
void tearDown() throws IOException {
    server.shutdown();
}

By starting the MockWebServer, you can obtain the URL and configure the WebClient to point to this URL. After each test, I shut down the server.

Within a test, you can return a mock response by using the server.enqueue() method:

Path path = Paths.get("src/test/resources/dummy-product.json");
String json = Files.readString(path);
server.enqueue(new MockResponse()
    .addHeader("Content-Type", "application/json")
    .setBody(json));

To write assertions based on the request, you can call server.takeRequest() after you made your call. For example:

RecordedRequest request = server.takeRequest();
assertThat(request.getPath()).isEqualTo("/products/1");

Since you have to wait until after you executed your WebClient call, you either have to call block() before calling server.takeRequest(), or you have to use StepVerifier’s verifyComplete() method. For example:

Path path = Paths.get("src/test/resources/dummy-product.json");
String json = Files.readString(path);
server.enqueue(new MockResponse()
    .addHeader("Content-Type", "application/json")
    .setBody(json));
StepVerifier
    // You usually put this `WebClient` method in some other class 
    // In that case, you write a test for that class
    // and then you just call the method that invokes `WebClient`
    .create(webClient
        .get()
        .uri(builder -> builder
            .pathSegment("products", "{id}")
            .build(Map.of("id", 1)))
        .retrieve()
        .bodyToMono(Product.class))
    .assertNext(product -> {
        assertThat(product.getId()).isEqualTo(1);
        // Write other assertions
    })
    .verifyComplete();
RecordedRequest request = server.takeRequest();
assertThat(request.getPath()).isEqualTo("/products/1");

Using the HTTP Exchange interface

Since Spring Boot 3, there’s also a new way to use WebClient. To use this new method, you have to create an interface and annotate the methods with @GetExchange, @PostExchange and so on. For example:

public interface ProductWebClientExchangeClient {
    @GetExchange("/products/{id}")
    Mono<ResponseEntity<Product>> findById(@PathVariable int id);

    @PostExchange("/products/add")
    Mono<ResponseEntity<Product>> create(@RequestBody CreateProductRequest request);

    @PutExchange("/products/{id}")
    Mono<ResponseEntity<Product>> update(@PathVariable int id, @RequestBody UpdateProductRequest request);
}

After that, you can create an instance of the ProductWebClientExchangeClient interface by using the following code:

@Bean
public ProductWebClientExchangeClient productWebClientExchangeClient(WebClient webClient) {
    WebClientAdapter adapter = WebClientAdapter.create(webClient);
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
    return factory.createClient(ProductWebClientExchangeClient.class);
}

Pros

The main advantages of WebClient is that it comes with a fluent API, that it’s reactive, non-blocking and asynchronous. In addition, it allows you to work with the HTTP exchange interface, which reduces boilerplate code even more.

Cons

The support for WebClient is currently still a bit limited. As I mentioned before, there’s no testing support, so you have to rely on external libraries.

In addition, the reactive ecosystem requires additional knowledge such as how to use its operators and so on. This usually isn’t as well known as using imperative programming or functional programming with streams.

RestClient

Setup

Since Spring 6.1 and Spring Boot 3.2 (currently only available as release candidate), RestClient was introduced. RestClient offers both the fluent API and the HTTP exchange interface from WebClient, but utilizes RestTemplate behind the screens. This makes it the ideal candidate for synchronous REST calls.

To use it, you can either bind it to an existing RestTemplate bean with RestClient.create(restTemplate), or you can create a new one with:

@Bean
public RestClient dummyAPIRestClient(RestClient.Builder builder) {
    return builder.baseUrl("https://dummyjson.com").build();
}

Retrieving data

Retrieving data can be done with:

Product product = restClient
    .get()
    .uri(uri -> uri.path("/products/1").build())
    .retrieve()
    .body(Product.class);

So like WebClient, it provides a get() method and a URI builder. After that, you can use the body() method to map the response body to a specific type.

You can also work with URL variables just like with RestTemplate and WebClient.

If you’re interested in the response headers, you can use the toEntity() method in stead of body().

Sending data

Sending data is similar to WebClient. By using the .body() method, you can pass a request body. For example:

CreateProductRequest request = new CreateProductRequest("Product");
ResponseEntity<Product> entity = restClient
    .post()
    .uri("/products/add")
    .body(request)
    .retrieve()
    .toEntity(Product.class);

Error handling

Error handling with RestClient can be done in either a imperative way as with RestTemplate, or in a fluent way as with WebClient. The fluent API for handling errors looks like this:

Product product = restClient
    .get()
    .uri(uri -> uri.path("/products/1").build())
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        DummyAPIError error = objectMapper.readValue(response.getBody(), DummyAPIError.class);
        throw new InvalidProductException(error);
    })
    .body(Product.class);

As you can see, it looks very similar to error handling with WebClient, but with a few exceptions. The first difference is that the handler method accepts both a request and response parameter, while WebClient only accepted a response parameter.

The second difference is that the response class does not provide a utility method to map the response body to a class. The ClientHttpResponse class only has a getBody() method that returns an InputStream. To convert this InputStream to a Java object, you need to map it by yourself by autowiring ObjectMapper and reading the InputStream.

Just like with WebClient, you can create a default status handler within the RestClient.Builder as well. For example:

@Bean
public RestClient dummyAPIRestClient(RestClient.Builder builder, ObjectMapper objectMapper) {
    return builder
        .baseUrl("https://dummyjson.com")
        .defaultStatusHandler(HttpStatusCode::is4xxClientError, (request, response) -> {
            DummyAPIError error = objectMapper.readValue(response.getBody(), DummyAPIError.class);
            throw new InvalidProductException(error);
        })
        .build();
}

Using the HTTP Exchange interface

Just like with WebClient, you can use the HTTP exchange interface with the new RestClient. The major difference is that your interface methods will no longer return a Publisher like Mono or Flux, but an object or ResponseEntity directly. For example:

public interface ProductRestClientExchangeClient {
    @GetExchange("/products/{id}")
    ResponseEntity<Product> findById(@PathVariable int id);

    @PostExchange("/products/add")
    ResponseEntity<Product> create(@RequestBody CreateProductRequest request);

    @PutExchange("/products/{id}")
    ResponseEntity<Product> update(@PathVariable int id, @RequestBody UpdateProductRequest request);
}

Registering a bean of this interface happens in a similar way as to WebClient. For example:

@Bean
public ProductRestClientExchangeClient productRestClientExchangeClient(RestClient restClient) {
    RestClientAdapter adapter = RestClientAdapter.create(restClient);
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
    return factory.createClient(ProductRestClientExchangeClient.class);
}

The major difference here is that you’ll use RestClientAdapter.create() in stead of WebClientAdapter.create().

Pros

The advantage of using RestClient is that it offers similar capabilities as WebClient, such as a modern, fluent API and the ability to use the HTTP exchange interface. This reduces boilerplate code dramatically.

In addition, since RestClient uses RestTemplate behind the screens, you can use the same interceptors, testing libraries and so on with this new RestClient.

Cons

The RestClient is still relatively new. Currently, there’s no release yet of Spring Boot that includes RestClient. You can try it out already by using the 3.2 release candidate.

In addition, RestClient relies on blocking, synchronous traffic in stead of reactive, asynchronous traffic like WebClient. The benefit of that is that synchronous flows are usually easier to understand, but the drawback is that you may encounter performance issues.

Deprecation?

There are a few mentions on the internet that RestTemplate is deprecated and that it shouldn’t be used. This is NOT true. This confusion exists due to earlier versions of the API docs containing a note that RestTemplate would be deprecated in the future (Source).

However, this note has been removed from the current API docs, and now it simply mentions that RestTemplate is in maintenance mode. This is because RestTemplate is considered feature-complete. The RestTemplate API does feel outdated, but it’s not deprecated and there’s nothing wrong with using feature-complete code!

If you do want to use a modern, fluent API, Spring does recommend using WebClient. However, I’d advice against using WebClient if you don’t plan on using reactive programming because you would be including a bunch of libraries just for that.

The next version of the API docs will include both WebClient and RestClient as modern alternatives to RestTemplate.

Conclusion

Spring offers three ways of calling REST APIs through RestTemplate, WebClient and RestClient. Neither of these are deprecated. if you don’t know what you should choose, the following is my opinion:

Choose RestTemplate if:

Choose WebClient if:

Choose RestClient if:

If you’re interested in seeing any of these code examples, be sure to check out GitHub.