Spring REST Client
Introduction
In modern web applications, services frequently need to communicate with each other through APIs. Spring provides robust tools for not only creating REST APIs but also consuming them. Spring REST Client functionality allows your application to interact with external RESTful services easily and effectively.
This guide will introduce you to the primary Spring REST client options:
- RestTemplate - The traditional synchronous REST client
- WebClient - The modern reactive alternative that supports both synchronous and asynchronous operations
By the end of this tutorial, you'll be able to integrate external REST services into your Spring applications with confidence.
RestTemplate Overview
RestTemplate
is Spring's traditional HTTP client for synchronous REST operations. Although it's being phased out in favor of WebClient
, it's still widely used and important to understand.
Setting Up RestTemplate
First, let's create a simple RestTemplate
bean in our Spring configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestClientConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Now we can inject this RestTemplate
instance wherever we need it in our application.
Basic CRUD Operations with RestTemplate
Let's explore how to perform common HTTP operations using RestTemplate
:
GET Request
Retrieving a resource is one of the most common operations:
@Service
public class UserService {
private final RestTemplate restTemplate;
private final String apiUrl = "https://api.example.com/users";
public UserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public User getUserById(Long id) {
return restTemplate.getForObject(apiUrl + "/{id}", User.class, id);
}
public ResponseEntity<User> getUserWithFullResponse(Long id) {
return restTemplate.getForEntity(apiUrl + "/{id}", User.class, id);
}
public List<User> getAllUsers() {
ResponseEntity<User[]> response = restTemplate.getForEntity(apiUrl, User[].class);
return Arrays.asList(response.getBody());
}
}
In these examples:
getForObject()
returns just the response bodygetForEntity()
returns aResponseEntity
that includes status code, headers, and body
POST Request
Creating a new resource:
public User createUser(User user) {
return restTemplate.postForObject(apiUrl, user, User.class);
}
public URI createUserAndGetLocation(User user) {
return restTemplate.postForLocation(apiUrl, user);
}
PUT Request
Updating an existing resource:
public void updateUser(Long id, User user) {
restTemplate.put(apiUrl + "/{id}", user, id);
}
DELETE Request
Removing a resource:
public void deleteUser(Long id) {
restTemplate.delete(apiUrl + "/{id}", id);
}
Handling Request Headers and Parameters
Often, you'll need to send custom headers or parameters with your requests:
public User getUserWithHeaders(Long id) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer your-token");
headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<User> response = restTemplate.exchange(
apiUrl + "/{id}",
HttpMethod.GET,
entity,
User.class,
id
);
return response.getBody();
}
Error Handling with RestTemplate
By default, RestTemplate
throws exceptions for HTTP error responses. You can customize this behavior:
@Bean
public RestTemplate restTemplateWithErrorHandler() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is5xxServerError();
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
// Custom error handling logic
if (response.getStatusCode().is5xxServerError()) {
throw new RuntimeException("Server error: " + response.getStatusCode());
}
}
});
return restTemplate;
}
WebClient: The Modern Alternative
WebClient
is Spring's non-blocking, reactive web client introduced in Spring 5. It supports both synchronous and asynchronous operations.
Setting Up WebClient
First, add the required dependency to your project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Then, create a WebClient
bean:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
Basic CRUD Operations with WebClient
Let's explore similar operations using WebClient
:
GET Request
@Service
public class ReactiveUserService {
private final WebClient webClient;
public ReactiveUserService(WebClient webClient) {
this.webClient = webClient;
}
// Asynchronous (non-blocking)
public Mono<User> getUserById(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);
}
// Synchronous (blocking)
public User getUserByIdBlocking(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.block(); // Blocks until complete
}
public Flux<User> getAllUsers() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class);
}
}
POST Request
public Mono<User> createUser(User user) {
return webClient.post()
.uri("/users")
.bodyValue(user)
.retrieve()
.bodyToMono(User.class);
}
PUT Request
public Mono<Void> updateUser(Long id, User user) {
return webClient.put()
.uri("/users/{id}", id)
.bodyValue(user)
.retrieve()
.bodyToMono(Void.class);
}
DELETE Request
public Mono<Void> deleteUser(Long id) {
return webClient.delete()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(Void.class);
}
Headers and Request Parameters
WebClient
makes adding headers and parameters very intuitive:
public Mono<User> getUserWithAuth(Long id, String token) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/users/{id}")
.queryParam("include", "profile,preferences")
.build(id))
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToMono(User.class);
}
Error Handling with WebClient
Error handling in WebClient
is done through operators in the reactive chain:
public Mono<User> getUserWithErrorHandling(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response ->
Mono.error(new ResourceNotFoundException("User not found with ID: " + id)))
.onStatus(HttpStatus::is5xxServerError, response ->
Mono.error(new ServiceUnavailableException("Server error occurred")))
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(5))
.doOnError(error -> log.error("Error fetching user: {}", error.getMessage()));
}
Real-World Example: Weather Service Client
Let's build a practical example that consumes a public weather API:
@Service
public class WeatherService {
private final WebClient webClient;
private final String apiKey;
public WeatherService(@Value("${weather.api.key}") String apiKey) {
this.apiKey = apiKey;
this.webClient = WebClient.builder()
.baseUrl("https://api.openweathermap.org/data/2.5")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
public Mono<WeatherResponse> getWeatherForCity(String city) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/weather")
.queryParam("q", city)
.queryParam("appid", apiKey)
.queryParam("units", "metric")
.build())
.retrieve()
.bodyToMono(WeatherResponse.class)
.doOnError(error -> log.error("Error fetching weather for city {}: {}",
city, error.getMessage()));
}
}
And a simple controller to expose this weather information:
@RestController
@RequestMapping("/api/weather")
public class WeatherController {
private final WeatherService weatherService;
public WeatherController(WeatherService weatherService) {
this.weatherService = weatherService;
}
@GetMapping("/{city}")
public Mono<WeatherDTO> getWeather(@PathVariable String city) {
return weatherService.getWeatherForCity(city)
.map(response -> new WeatherDTO(
response.getName(),
response.getMain().getTemp(),
response.getWeather().get(0).getDescription(),
response.getMain().getHumidity()
));
}
}
When to Use RestTemplate vs WebClient
-
Use RestTemplate when:
- You're working with a legacy Spring application
- You need simple synchronous requests
- You don't need non-blocking operations
-
Use WebClient when:
- You need to handle a high number of concurrent connections
- You need non-blocking, asynchronous communication
- You're working with reactive streams or reactive programming
- You're building a new application (recommended approach by Spring)
Summary
In this guide, we've explored Spring's REST client capabilities:
- RestTemplate: The traditional synchronous client that offers a simple API for REST operations
- WebClient: The modern reactive client that supports both synchronous and asynchronous communication
Both tools enable your Spring applications to consume RESTful APIs effectively, but WebClient represents the future direction for Spring REST clients with its non-blocking capabilities, fluent API, and reactive support.
Additional Resources
- Official Spring RestTemplate Documentation
- Official Spring WebClient Documentation
- Baeldung's RestTemplate Guide
- Baeldung's WebClient Guide
Exercises
- Create a client that consumes the JSONPlaceholder API to fetch and display posts
- Build a GitHub repository browser using GitHub's REST API and WebClient
- Implement error handling for both RestTemplate and WebClient that gracefully handles timeouts and service unavailability
- Create a service that aggregates data from multiple API endpoints and returns a combined result
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)