Most real backend systems do not stand alone. They store their own data, but they also talk to payment providers, map services, weather services, email services, identity providers, analytics tools, and internal systems owned by other teams.
In the Core Program's Week 9 - Networking and APIs, you already consumed APIs as an external client: you used JavaScript, curl, and Postman to send HTTP requests and inspect JSON responses. In Week 4 - Rest APIs, you switched sides and learned how to build REST endpoints with Spring Boot. In the Integrating with Spring Boot chapter, you saw how Spring Boot uses the application context, beans, configuration classes, and constructor injection to wire an application together.
Now those ideas come together: your Spring Boot application can act as both an API server and an API client.
This allows you to build richer features by integrating with external services (payment providers, weather services, maps, email tools, etc.) instead of implementing everything yourself. The quality of these integrations often determines how reliable and maintainable your application becomes.
Your application can expose endpoints to its own users while also calling another HTTP API behind the scenes. For example, your backend might expose GET /weather?city=Amsterdam, call a weather provider, translate the provider's response into your own DTO, and return a response shaped for your frontend.
<aside> 💭
Think of one app you use often. Which features probably depend on another company's API?
</aside>
When you integrate with an external API, your Spring Boot app has a server for receiving requests and a client for sending outgoing HTTP requests:
flowchart LR
frontend["Browser / Frontend"] ==> Server
subgraph Backend["Spring Boot API"]
direction LR
Server
Client["RestClient<br/>(@Bean + injected)"]
Server <--> Client
end
Client ==> PaymentProcessorAPI["Payment Provider<br/>(external)"]
Client ==> MapsAPI["Maps / Weather API<br/>(external)"]
style frontend fill:#e8f3ff,stroke:#2f80ed,stroke-width:1.5px,color:#123
style Backend fill:#eaf8ed,stroke:#2f9e44,stroke-width:1.5px,color:#123
classDef internal fill:#fafffb,stroke:#000000,stroke-width:1.5px,color:#000000
classDef external fill:#f3edff,stroke:#7950f2,stroke-width:1.5px,color:#123
class PaymentProcessorAPI,MapsAPI external
class Server,Client internal
RestTemplate vs RestClientSpring has had more than one synchronous HTTP client over time. You will often see RestTemplate in older tutorials and existing codebases. For current Spring applications, prefer RestClient.
| Client → | RestTemplate | RestClient |
|---|---|---|
| Status | Older, still found in many projects | Modern synchronous client |
| Style | Method-based (see comparison below) | Fluent chain |
| Note | Common in older codebases | Introduced in Spring Boot 3.2+. Will replace RestTemplate |
RestTemplate, method-based:
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> res =
restTemplate.getForEntity(
"https://...", User.class);
RestClient, fluent chain:
// restClient bean wired by Spring to a field
User user = restClient.get()
.uri("https://...")
.retrieve()
.body(User.class);
<aside> 💡
RestClient is available through the Spring Web dependency you added earlier with Spring Initializr in Spring Boot Setup.
However, for full auto-configuration support, you may need to add another Spring Boot dependency. We’ll cover that in the “**Configuring RestClient”** section below.
For now, just make sure you’ve selected a recent enough Spring Boot version which includes RestClient .
</aside>
RestClient is a modern synchronous HTTP client. That means the Java thread executing the request waits for the response before continuing. If you need a refresher, revisit the Synchronous vs Asynchronous Code chapter from the Core program.
<aside> 💡
Don’t underestimate synchronous clients. Many real-world applications use them successfully. When combined with resilience strategies such as timeouts, circuit breakers, and retries, they can easily handle hundreds of requests per second.
Async clients (such as WebClient in reactive mode) are powerful, but they add complexity to your application. You may still need them in certain scenarios, for example:
Since synchronous clients are simpler and easier to debug, we’ll only use them here. You can always explore asynchronous clients later when the need arises.
</aside>
RestClientDo not create a new RestClient inside every method. HTTP clients are relatively heavyweight objects. Creating them repeatedly wastes memory and prevents proper connection pooling, keep-alive behavior, and other optimizations that Spring manages when you configure the client once as a bean.
You can configure your RestClient as a Spring bean and reuse it across the application. This follows the Dependency Injection approach covered in the Integrating with Spring Boot chapter.
First, configure your client in a @Bean method:
package net.hackyourfuture.weatherapi.weather.client;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;
@Configuration
public class MeteoApiConfiguration {
@Bean
public RestClient meteoRestClient(RestClient.Builder builder) {
return builder
.baseUrl("<https://api.open-meteo.com>")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
Spring beans are singletons by default. This means they’re instantiated only once, the first time they’re required, and, thereafter, the same instance is reused across the application.
flowchart LR
subgraph Config["MeteoApiConfiguration.java<br/>@Configuration"]
direction LR
BeanMethod["@Bean method to create RestClient with baseUrl + headers"]-->Bean["@Bean"]
end
BeanMethod
Bean-->|registered in| Spring["Spring Container"]
style Config fill:#e0f2fe,stroke:#000
classDef beanStyle fill:#dcfce7,stroke:#000
style Spring fill:#fef3c7,stroke:#d97706
class Bean beanStyle
class BeanMethod beanStyle
If IntelliJ is giving you the below error:

You need to add the Spring Boot starter dependency that autoconfigures a RestClient.Builder :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
Then, you can retrieve your configured RestClient bean from the Spring container:
package net.hackyourfuture.weatherapi.weather.client;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class MeteoClient {
private final RestClient meteoRestClient;
public MeteoClient(@Qualifier("meteoRestClient") RestClient meteoRestClient) {
this.meteoRestClient = meteoRestClient;
}
}
@Qualifier is used when there are multiple beans of the same type in the Spring container (in this case, multiple RestClient beans). The annotation tells Spring exactly which bean to inject by specifying the bean name as its value (that’s typically the @Bean method name) .
flowchart LR
subgraph Spring["Spring Container"]
direction LR
Bean@{ shape: processes, label: "RestClient @Beans" }
end
Bean -->|qualify with 'meteoRestClient'| Service["MeteoClient.java<br/>@Service"]
style Bean fill:#dcfce7,stroke:#000
style Spring fill:#fef3c7,stroke:#d97706
style Service fill:#f3edff,stroke:#7950f2
The great thing about this approach is that the service doesn’t need to know how the RestClient is built. It only knows that Spring injects a properly configured instance through the constructor, allowing the service to focus on its own use case instead of client setup details.
For the first example, you will use Open-Meteo.com, a public weather API that does not require an API key for basic use.
Example URL:
**<https://api.open-meteo.com/v1/forecast>
?latitude=52.37
&longitude=4.90
¤t=temperature_2m,wind_speed_10m**
Opening this URL in the browser gives a response with this shape:
{
"latitude": 52.366,
"longitude": 4.901,
"generationtime_ms": 0.0779628753662109,
"utc_offset_seconds": 0,
"timezone": "GMT",
"timezone_abbreviation": "GMT",
"elevation": 11,
"current_units": {
"time": "iso8601",
"interval": "seconds",
"temperature_2m": "°C",
"wind_speed_10m": "km/h"
},
"current": {
"time": "2026-06-06T12:00", // Will be the current time
"interval": 900,
"temperature_2m": 15.2,
"wind_speed_10m": 27.4
}
}
<aside> ⌨️
Hands on: Open the Open-Meteo URL in your browser or Postman. Experiment with changing the latitude and longitude and observing the results.
</aside>
Java code usually uses camelCase, but this API uses snake_case for fields like temperature_2m.
Use @JsonProperty to map the JSON field name to a Java record component:
package net.hackyourfuture.weatherapi.weather.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public record MeteoResponse(double latitude, double longitude, CurrentWeather current) {
public record CurrentWeather(
String time,
@JsonProperty("temperature_2m") double temperature,
@JsonProperty("wind_speed_10m") double windSpeed) {}
}
Now the client service can send a GET request and ask Jackson to deserialize the JSON response into MeteoResponse:
package net.hackyourfuture.weatherapi.weather.client;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import net.hackyourfuture.weatherapi.weather.dto.MeteoResponse;
@Service
public class MeteoClient {
private final RestClient meteoRestClient;
public MeteoClient(@Qualifier("meteoRestClient") RestClient meteoRestClient) {
this.meteoRestClient = meteoRestClient;
}
public MeteoResponse getCurrentWeather(double latitude, double longitude) {
return meteoRestClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/forecast")
.queryParam("latitude", latitude)
.queryParam("longitude", longitude)
.queryParam("current", "temperature_2m,wind_speed_10m")
.build())
.retrieve()
.body(MeteoResponse.class);
}
}
sequenceDiagram
box lightblue Internal
participant WeatherController
participant WeatherService
participant MeteoClient
end
box lavender External
participant OpenMeteo as Open-Meteo API<br/>(external)
end
WeatherController->>WeatherService: getCurrentWeather(lat, lon)
WeatherService->>MeteoClient: getCurrentWeather(lat, lon)
MeteoClient->>OpenMeteo: HTTP GET /v1/forecast with queryParams
OpenMeteo-->>MeteoClient: JSON response
MeteoClient-->>WeatherService: deserialized to MeteoResponse
WeatherService->>WeatherService: map to your own API response DTO
WeatherService-->>WeatherController: your own API response DTO
The uriBuilder builds the path and query string safely. This is easier to read and less error-prone than manually joining strings like "?latitude=" + latitude + "&longitude=" + longitude.
retrieve() sends the request and prepares the response for handling. body(MeteoResponse.class) tells Spring to convert the JSON response body into a MeteoResponse object.
💬 Why use DTOs for external API responses?
For clients, POST requests work almost exactly like the GET requests you just saw. The main difference is that you need to send data in the request body. Jackson makes this easy by serializing your object into a JSON string for the request body.
For demonstration, here’s a minimal example independent of our Meteo API integration:
// Example request DTO to be serialized
public record CreatePostRequest(String title, String body, int userId) {}
Then, to integrate the POST call with an external API client:
@Service
public class PostClient {
private final RestClient externalApiClient;
public PostClient(@Qualifier("someExternalAPIClient") RestClient externalApiClient) {
this.externalApiClient = externalApiClient;
}
public CreatedPostResponse createPost(CreatePostRequest request) {
return externalApiClient
.post()
.uri("/posts")
.contentType(MediaType.APPLICATION_JSON) // Tell the server the request body's content-type
.body(request) // Jackson serialization
.retrieve()
.body(CreatedPostResponse.class); // Jackson deserialization
}
}
The important pieces are:
| --- | --- | --- |
Now that you’ve seen how to handle POST requests when needed, let’s return to our Meteo API integration.
External APIs do not always return 200 OK. They can reject your request, fail internally, rate-limit you, become unreachable, or return data that does not match what your code expects.
With RestClient, retrieve() treats unsuccessful HTTP responses differently from successful responses. You can add onStatus handlers to translate provider HTTP errors into exceptions that make sense inside your application.
Start with a small custom exception:
package net.hackyourfuture.weatherapi.shared.exception;
public class ExternalApiException extends RuntimeException {
public ExternalApiException(String message) { super(message); }
public ExternalApiException(String message, Throwable cause) { super(message, cause); }
}
Then handle 4xx and 5xx responses:
package net.hackyourfuture.weatherapi.weather.client;
import net.hackyourfuture.weatherapi.shared.exception.ExternalApiException;
import net.hackyourfuture.weatherapi.weather.dto.MeteoResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
@Service
public class MeteoClient {
private final RestClient meteoRestClient;
public MeteoClient(@Qualifier("meteoRestClient") RestClient meteoRestClient) {
this.meteoRestClient = meteoRestClient;
}
public MeteoResponse getCurrentWeather(double latitude, double longitude) {
try {
return meteoRestClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/forecast")
.queryParam("latitude", latitude)
.queryParam("longitude", longitude)
.queryParam("current", "temperature_2m,wind_speed_10m")
.build())
.retrieve()
.onStatus(HttpStatusCode::isError, (_, response) -> {
throw new ExternalApiException("Weather provider error: " + response.getStatusCode());
})
.body(MeteoResponse.class);
} catch (RestClientException exception) {
throw new ExternalApiException(
"The weather provider could not be reached or returned an unexpected response.",
exception
);
}
}
}
These failure categories can mean different things:
| --- | --- | --- |
<aside> ⚠️
</aside>
In a real application, you would often catch ExternalApiException in a service or @ControllerAdvice class and translate it to a controlled response from your own API.
💬 Why not call the external API directly from a controller?
External APIs can be slow or unreachable. Without timeouts, a backend thread may wait much longer than your users expect. Enough stuck requests can make your own application slow, even when the problem is in another system.
Two timeout terms matter most: