Consuming External APIs
Call external services from a Spring Boot application using RestTemplate and the modern WebClient, including error handling and DTO mapping.
Introduction
Most real applications do not exist in isolation - they call payment gateways, weather services, other internal microservices, or third-party APIs. Spring Boot offers two main HTTP client options for this: the older, simpler RestTemplate, and the modern, non-blocking WebClient. In this lesson you will call an external API with both, map the response into your own DTOs, and handle failures gracefully.
- The difference between RestTemplate and WebClient.
- How to make a GET request with RestTemplate.
- How to make the same call with WebClient.
- How to map a JSON response into your own DTO class.
- How to handle errors from an external service gracefully.
RestTemplate vs WebClient
RestTemplate has been part of Spring for years and is simple to use, but it is blocking and is now in maintenance mode - the Spring team recommends WebClient for new code. WebClient is part of Spring WebFlux, supports both blocking and non-blocking usage, and is the forward-looking choice even in a traditional (non-reactive) Spring Boot application.
| Aspect | RestTemplate | WebClient |
|---|---|---|
| Style | Blocking, synchronous | Non-blocking, reactive (usable synchronously too) |
| Status | Maintenance mode, not recommended for new code | Actively developed, recommended default |
| Dependency | Included with Spring Web | Requires spring-boot-starter-webflux |
Calling an API with RestTemplate
RestTemplate is registered as a bean and then used to make GET, POST, and other HTTP calls with a small, direct API.
@Configurationpublic class RestClientConfig {
@Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .connectTimeout(Duration.ofSeconds(3)) .readTimeout(Duration.ofSeconds(5)) .build(); }}
@Servicepublic class WeatherClient {
private final RestTemplate restTemplate;
public WeatherClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; }
public WeatherResponse getWeather(String city) { String url = "https://api.example.com/weather?city={city}"; return restTemplate.getForObject(url, WeatherResponse.class, city); }}Click Run to see what this code prints.
Calling an API with WebClient
WebClient is built fluently and, even in a traditional servlet application, can be used synchronously by calling .block() on the result.
@Configurationpublic class WebClientConfig {
@Bean public WebClient weatherWebClient(WebClient.Builder builder) { return builder.baseUrl("https://api.example.com").build(); }}
@Servicepublic class WeatherClient {
private final WebClient webClient;
public WeatherClient(WebClient weatherWebClient) { this.webClient = weatherWebClient; }
public WeatherResponse getWeather(String city) { return webClient.get() .uri(uriBuilder -> uriBuilder.path("/weather").queryParam("city", city).build()) .retrieve() .bodyToMono(WeatherResponse.class) .block(); }}Click Run to see what this code prints.
Mapping the Response to a DTO
Both clients deserialize the JSON response directly into a Java type using Jackson, the same library Spring uses for its own REST controllers. A record works well as a lightweight response DTO.
public record WeatherResponse(String city, double tempCelsius, String condition) {}Map the external response into your own DTO type instead of passing it straight through from your controller. This insulates your API from the third party's field names changing, and lets you shape the response to fit your own contract.
Handling Errors
External services fail - they time out, return 500s, or go down entirely. Wrap calls so a failure does not crash your own request and, ideally, degrades gracefully.
public WeatherResponse getWeather(String city) { try { return webClient.get() .uri(uriBuilder -> uriBuilder.path("/weather").queryParam("city", city).build()) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new WeatherServiceException("Invalid request to weather API"))) .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new WeatherServiceException("Weather API is unavailable"))) .bodyToMono(WeatherResponse.class) .block(); } catch (WebClientRequestException ex) { throw new WeatherServiceException("Could not reach weather API", ex); }}Click Run to see what this code prints.
Common Mistakes
- Calling new RestTemplate() directly instead of injecting a configured bean, which loses timeout and connection pool settings.
- Never setting a connection or read timeout, letting a slow external service hang a request indefinitely.
- Passing the raw external response straight back from your own controller instead of mapping it to your own DTO.
- Not handling 4xx/5xx responses from the external API, letting an unchecked exception bubble up as a generic 500.
- Using WebClient reactively throughout a call chain that is otherwise entirely blocking, mixing paradigms inconsistently.
Best Practices
- Prefer WebClient for new code, even if used synchronously with .block().
- Always configure explicit connection and read timeouts.
- Map external responses into your own DTOs rather than exposing them directly.
- Handle both client and server error status codes from the external API explicitly.
- Consider a resilience library (like Resilience4j) for retries and circuit breaking around unreliable external calls.
Frequently Asked Questions
Not formally deprecated as of recent Spring versions, but it is officially in maintenance mode - no new features are planned, and the Spring team explicitly recommends WebClient for new development.
No. You can add spring-boot-starter-webflux purely to get the WebClient class and still build a traditional, blocking Spring MVC application - just call .block() where you need a synchronous result.
Use a tool like WireMock or MockWebServer to stand up a fake HTTP server in your test, or mock the client class itself if the test only needs to verify your own logic around the call.
Key Takeaways
- RestTemplate is simple but in maintenance mode; WebClient is the recommended modern choice.
- Register HTTP clients as beans so timeouts and other settings are configured consistently.
- Map external JSON responses into your own DTOs instead of exposing them directly.
- Handle 4xx and 5xx responses from external services explicitly rather than letting errors propagate unchecked.
- A slow or failing external service should degrade gracefully, not take your own API down with it.
Summary
Calling external APIs is a routine part of most real Spring Boot applications. Favor WebClient for new code, always configure timeouts, map responses into your own DTOs, and handle failures explicitly so a struggling third-party service never becomes your own outage.