“Your Spring Boot app can talk to the world, not just your browser! Think of it as teaching your monkey to order bananas from an online store, not just pick them from a tree.” 🐒🛒
You’ve built robust APIs, but modern applications rarely live in isolation. They need to fetch data, trigger actions, or interact with other services—often via their REST APIs. Enter Spring Boot’s RestClient
, the modern, non-blocking way to consume external HTTP services. Let’s learn to make your application a fluent speaker in the language of the web!
🚩 Prerequisites
- Java Development Kit (JDK) 17+
- Maven or Gradle
- A basic Spring Boot REST project (like the one from “How to set up your first Spring Boot project”).
- Familiarity with REST API concepts (GET, POST, JSON).
1️⃣ Why RestClient? (Goodbye RestTemplate!)
For years, RestTemplate
was the go-to for synchronous HTTP calls. But it’s now in maintenance mode, superseded by more modern alternatives:
WebClient
: Reactive, non-blocking, and powerful. Great for complex async scenarios.RestClient
: New in Spring Framework 6 (Spring Boot 3.2+). It’s a synchronous, blocking client with a fluent API, designed to be a modern replacement forRestTemplate
, offering a similar feel but built onWebClient
‘s infrastructure.
For most simple, blocking API calls, RestClient
is your new best friend.
2️⃣ Add Dependencies (If Not Already There)
If you’re using Spring Boot 3.2+ and have spring-boot-starter-web
, RestClient
is already available. If not, ensure you have the Web Starter:
Maven (pom.xml
)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Gradle (build.gradle
)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
3️⃣ Configure RestClient as a Bean
It’s best practice to configure and provide RestClient
as a bean, allowing for easy injection and customization (e.g., base URL, timeouts).
// src/main/java/com/example/demo/config/AppConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class AppConfig {
@Bean
public RestClient externalApiService(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com") // Base URL for the external API
.build();
}
// You can also create a generic RestClient builder for other services if needed
@Bean
public RestClient.Builder restClientBuilder() {
return RestClient.builder();
}
}
RestClient.Builder
: This is automatically provided by Spring Boot.baseUrl()
: Crucial for setting the common part of your external API’s URL.
4️⃣ Create a Service to Consume the API (The Banana Supplier!)
Let’s imagine an external API that provides details about bananas. We’ll define a simple DTO to match its response structure.
// src/main/java/com/example/demo/dto/ExternalBananaDto.java
package com.example.demo.dto;
public record ExternalBananaDto(
String id,
String type,
String color,
int sweetnessLevel
) {}
// src/main/java/com/example/demo/service/ExternalBananaService.java
package com.example.demo.service;
import com.example.demo.dto.ExternalBananaDto;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.http.MediaType;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
public class ExternalBananaService {
private final RestClient restClient;
public ExternalBananaService(RestClient externalApiService) {
this.restClient = externalApiService;
}
public List<ExternalBananaDto> getAllExternalBananas() {
return Arrays.asList(restClient.get()
.uri("/bananas") // Relative to the baseUrl
.accept(MediaType.APPLICATION_JSON)
.retrieve() // Execute the request
.body(ExternalBananaDto[].class)); // Convert response body to an array
}
public Optional<ExternalBananaDto> getExternalBananaById(String bananaId) {
return restClient.get()
.uri("/bananas/{id}", bananaId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(ExternalBananaDto.class) // Retrieve as ResponseEntity
.map(responseEntity -> {
if (responseEntity.getStatusCode().is2xxSuccessful()) {
return Optional.ofNullable(responseEntity.getBody());
}
return Optional.<ExternalBananaDto>empty(); // Handle non-2xx status explicitly
})
.orElse(Optional.empty()); // Handle no response or other issues
}
public ExternalBananaDto createExternalBanana(ExternalBananaDto newBanana) {
return restClient.post()
.uri("/bananas")
.contentType(MediaType.APPLICATION_JSON)
.body(newBanana)
.retrieve()
.body(ExternalBananaDto.class);
}
}
get()
,post()
,put()
,delete()
: Start building your HTTP request.uri(...)
: Specifies the path, can use placeholders like{id}
.accept(...)
,contentType(...)
: Set request headers likeAccept
andContent-Type
.retrieve()
: Initiates the exchange and allows you to specify how to handle the response.body(Class<T>)
: Converts the response body directly to your desired object.toEntity(Class<T>)
: Returns aResponseEntity<T>
, giving you access to headers and status codes.
5️⃣ Expose via a Controller (Your Banana Portal!)
Finally, expose an endpoint in your application that uses the ExternalBananaService
:
// src/main/java/com/example/demo/controller/LocalBananaController.java
package com.example.demo.controller;
import com.example.demo.dto.ExternalBananaDto;
import com.example.demo.service.ExternalBananaService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/local-api/external-bananas")
public class LocalBananaController {
private final ExternalBananaService externalBananaService;
public LocalBananaController(ExternalBananaService externalBananaService) {
this.externalBananaService = externalBananaService;
}
@GetMapping
public List<ExternalBananaDto> getExternalBananas() {
return externalBananaService.getAllExternalBananas();
}
@GetMapping("/{id}")
public ResponseEntity<ExternalBananaDto> getExternalBanana(@PathVariable String id) {
return externalBananaService.getExternalBananaById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ExternalBananaDto createExternalBanana(@RequestBody ExternalBananaDto newBanana) {
return externalBananaService.createExternalBanana(newBanana);
}
}
6️⃣ Error Handling with RestClient
RestClient
provides excellent error handling capabilities. By default, it throws a RestClientException
for 4xx and 5xx responses. You can customize this:
Using onStatus()
// In ExternalBananaService
public Optional<ExternalBananaDto> getExternalBananaByIdWithCustomError(String bananaId) {
return restClient.get()
.uri("/bananas/{id}", bananaId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus.NOT_FOUND, (request, response) -> {
throw new CustomNotFoundException("Banana not found on external API: " + bananaId);
})
.onStatus(HttpStatus.INTERNAL_SERVER_ERROR, (request, response) -> {
throw new RuntimeException("External API server error!");
})
.body(ExternalBananaDto.class) // Direct conversion will work if status is OK
.map(Optional::of) // Wrap in Optional
.orElse(Optional.empty()); // If body is null or 4xx/5xx handled, return empty
}
onStatus(HttpStatus, (req, res) -> ...)
: Lets you define specific behavior for certain HTTP status codes.
💡 Monkey-Proof Tips
- Timeouts: Always configure timeouts for external API calls to prevent your application from hanging. Use
builder.requestFactory(...)
to set read and connection timeouts. - Retries/Circuit Breakers: For production, integrate with resilience patterns like retries (e.g., Spring Retry) or circuit breakers (e.g., Resilience4j) to handle transient failures in external services.
- Authentication: Include authentication headers (API keys, JWTs) using
.header("Authorization", "Bearer YOUR_TOKEN")
if the external API requires it. - Testing: When testing services that use
RestClient
, mock theRestClient
itself or use libraries like WireMock to simulate external API responses. - Logging: Add logging interceptors to your
RestClient
builder to log requests and responses for debugging.
🚀 Challenge
- Integrate with a real public API: Replace “https://api.example.com” with a real, free public API (e.g., JSONPlaceholder for dummy data, or a weather API). Adjust your DTOs and service methods accordingly.
- Implement Request Timeouts: Configure your
RestClient
bean to have a connection timeout of 1 second and a read timeout of 5 seconds. - Custom Error Handling for 401 Unauthorized: Add an
onStatus()
handler forHttpStatus.UNAUTHORIZED
(401) that throws a customExternalServiceUnauthorizedException
. Test this by making a request that would trigger a 401 (if the API supports it, or by simulating it with a test setup).
👏 You’ve now taught your Spring Boot application to seamlessly communicate with external REST APIs using RestClient
! Your app is no longer an island; it’s a social butterfly, exchanging data with services across the web. Keep connecting, and keep building interconnected Java applications!