Skip to main content

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:

  1. RestTemplate - The traditional synchronous REST client
  2. 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:

java
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:

java
@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 body
  • getForEntity() returns a ResponseEntity that includes status code, headers, and body

POST Request

Creating a new resource:

java
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:

java
public void updateUser(Long id, User user) {
restTemplate.put(apiUrl + "/{id}", user, id);
}

DELETE Request

Removing a resource:

java
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:

java
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:

java
@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:

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

Then, create a WebClient bean:

java
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

java
@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

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

PUT Request

java
public Mono<Void> updateUser(Long id, User user) {
return webClient.put()
.uri("/users/{id}", id)
.bodyValue(user)
.retrieve()
.bodyToMono(Void.class);
}

DELETE Request

java
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:

java
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:

java
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:

java
@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:

java
@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:

  1. RestTemplate: The traditional synchronous client that offers a simple API for REST operations
  2. 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

Exercises

  1. Create a client that consumes the JSONPlaceholder API to fetch and display posts
  2. Build a GitHub repository browser using GitHub's REST API and WebClient
  3. Implement error handling for both RestTemplate and WebClient that gracefully handles timeouts and service unavailability
  4. 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! :)