With microservice architectures and external API integrations now commonplace, calling external REST APIs from Spring Boot applications has become a very common requirement. Spring Boot provides two primary HTTP clients — RestTemplate and WebClient — and choosing between them can sometimes be a challenge.

This article covers everything from the basics of RestTemplate and WebClient, to guidelines for choosing between them, to the timeout configuration and error handling that are essential in production environments.

Differences Between RestTemplate and WebClient

Let’s start by understanding the characteristics of each HTTP client.

RestTemplate is a synchronous HTTP client that has been part of Spring Framework for a long time. It features a simple, intuitive API and has been used in many projects. However, as of Spring 5, it is in maintenance mode, and WebClient is recommended for new development.

WebClient is an asynchronous, reactive HTTP client introduced in Spring 5. Provided as part of Spring WebFlux, it offers high throughput through non-blocking I/O and a flexible API. It can also be used synchronously, making it possible to introduce it incrementally into existing codebases.

For new projects, choosing WebClient is generally the right call. However, if existing RestTemplate code is working without issues, there is no need to migrate forcefully.

Setting Up Dependencies

RestTemplate is included in spring-boot-starter-web, so no additional dependencies are required for a standard Spring Boot web application.

To use WebClient, add spring-boot-starter-webflux.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

For Gradle, write it as follows.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

Adding WebFlux does not conflict with Spring MVC, so existing Controllers are unaffected.

Basic Usage of RestTemplate

To use RestTemplate, first register it as a Bean.

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

To fetch data with a GET request, use getForObject or getForEntity.

@Service
public class ApiService {
    
    private final RestTemplate restTemplate;
    
    public ApiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    public User getUser(Long id) {
        String url = "https://api.example.com/users/" + id;
        return restTemplate.getForObject(url, User.class);
    }
}

To send data with a POST request, use postForObject.

public User createUser(User user) {
    String url = "https://api.example.com/users";
    return restTemplate.postForObject(url, user, User.class);
}

If you also need the response status code or headers, use getForEntity or postForEntity, which return a ResponseEntity.

Timeout Configuration for RestTemplate

Always configure timeouts in production. Without them, your application will wait indefinitely if an external API becomes unresponsive.

To set timeouts, you need the Apache HttpClient dependency.

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>

For Gradle, write it as follows.

implementation 'org.apache.httpcomponents.client5:httpclient5'

Once the dependency is added, you can configure timeouts using HttpComponentsClientHttpRequestFactory.

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setConnectTimeout(3000);  // Connection timeout (3 seconds)
    factory.setReadTimeout(5000);     // Read timeout (5 seconds)
    return new RestTemplate(factory);
}

Using RestTemplateBuilder allows for more concise configuration.

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder
            .setConnectTimeout(Duration.ofSeconds(3))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
}

The connection timeout limits how long to wait for a connection to be established with the server, while the read timeout limits how long to wait for the response to be fully read. Set appropriate values based on the characteristics of the target API.

Error Handling for RestTemplate

RestTemplate throws an exception when it receives a 4xx or 5xx response. Make sure to handle these appropriately.

public User getUser(Long id) {
    try {
        String url = "https://api.example.com/users/" + id;
        return restTemplate.getForObject(url, User.class);
    } catch (HttpClientErrorException e) {
        // 4xx errors (client errors)
        if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
            throw new UserNotFoundException("User not found: " + id);
        }
        throw e;
    } catch (HttpServerErrorException e) {
        // 5xx errors (server errors)
        throw new ExternalApiException("API server error", e);
    }
}

For more flexible error handling, you can customize ResponseErrorHandler. By registering a custom handler via setErrorHandler when configuring RestTemplate, you can apply consistent error handling across all requests.

Basic Usage of WebClient

WebClient instances are created using WebClient.builder().

@Configuration
public class WebClientConfig {
    
    @Bean
    public WebClient webClient() {
        return WebClient.builder()
                .baseUrl("https://api.example.com")
                .build();
    }
}

GET requests are implemented as follows. Using block() retrieves the result synchronously.

@Service
public class ApiService {
    
    private final WebClient webClient;
    
    public ApiService(WebClient webClient) {
        this.webClient = webClient;
    }
    
    public User getUser(Long id) {
        return webClient.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class)
                .block();
    }
}

For POST requests, set the request body using bodyValue.

public User createUser(User user) {
    return webClient.post()
            .uri("/users")
            .bodyValue(user)
            .retrieve()
            .bodyToMono(User.class)
            .block();
}

For asynchronous processing, you can return the Mono directly without calling block().

public Mono<User> getUserAsync(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .bodyToMono(User.class);
}

Timeout Configuration for WebClient

WebClient timeouts are configured through Reactor Netty settings.

import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import java.time.Duration;

@Bean
public WebClient webClient() {
    HttpClient httpClient = HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
            .responseTimeout(Duration.ofSeconds(5));
    
    return WebClient.builder()
            .baseUrl("https://api.example.com")
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
}

As with RestTemplate, you can configure both connection and read timeouts. If you need to override the timeout for an individual request, you can also use the timeout() operator.

Error Handling for WebClient

WebClient supports per-status-code error handling using onStatus.

public User getUser(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, response ->
                    response.bodyToMono(String.class)
                            .map(body -> new UserNotFoundException("User not found: " + id))
            )
            .onStatus(HttpStatusCode::is5xxServerError, response ->
                    Mono.error(new ExternalApiException("API server error"))
            )
            .bodyToMono(User.class)
            .block();
}

When you want to return a fallback value on error, onErrorResume is useful.

public User getUserWithFallback(Long id) {
    return webClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .bodyToMono(User.class)
            .onErrorResume(e -> Mono.just(new User("Unknown")))
            .block();
}

Which Should You Choose?

For new projects or when undertaking large-scale refactoring, WebClient is the recommended choice given its longevity. It is especially well-suited for scenarios that require asynchronous processing or high throughput.

On the other hand, if RestTemplate is working fine in an existing project, it may be wiser to spend time on higher-priority improvements rather than migrating. If simple synchronous processing is sufficient, or if the team is unfamiliar with reactive programming, RestTemplate is perfectly viable in practice.

RestTemplate is in maintenance mode, but it will not suddenly become unavailable. Make the right judgment call based on your requirements and team situation.

Tips for Production Configuration

Externalizing timeout values in application.properties makes it easy to adjust them per environment.

api.client.connect-timeout=3000
api.client.read-timeout=5000
api.client.base-url=https://api.example.com

Load these values with @Value and use them in your Bean configuration. For more on working with property files, see Managing Configuration with Spring Boot Property Files.

When calling multiple external APIs, creating a dedicated Bean for each and using @Qualifier to distinguish them makes them easier to manage.

For error handling, how exceptions thrown at the API client layer are handled at the controller layer is also important. See How to Handle REST API Exceptions Cleanly in Spring Boot for details.

Summary

This article covered how to call REST APIs from Spring Boot using RestTemplate and WebClient.

RestTemplate is simple and battle-tested, but WebClient is recommended for new development. Whichever you choose, always implement timeout configuration and error handling.

For guidance on testing the code you implement, Spring Boot Testing Basics with JUnit and Mockito is a helpful reference.

Integrating with external APIs is a requirement that comes up frequently in real-world development, so mastering the content in this article should give you the confidence to implement it reliably.