RestTemplate to RestClient Migration Skill
Overview
Spring Framework 6.1 (Spring Boot 3.2+) introduces RestClient, a modern, fluent API for synchronous HTTP requests that replaces the older RestTemplate. While RestTemplate still works, RestClient is the recommended approach for new code.
Key Differences
| Feature | RestTemplate | RestClient |
|---|---|---|
| API Style | Template methods | Fluent builder |
| Configuration | Constructor injection | Builder pattern |
| Error handling | ResponseErrorHandler | Status handlers |
| Type safety | Limited | Better with generics |
Migration Examples
1. Basic GET Request
Before (RestTemplate)
java
@Service
public class ExternalApiService {
private final RestTemplate restTemplate;
public ExternalApiService() {
this.restTemplate = new RestTemplate();
}
public Map<String, Object> getUser(String userId) {
String url = "https://api.example.com/users/" + userId;
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
return response.getBody();
}
}
After (RestClient)
java
@Service
public class ExternalApiService {
private final RestClient restClient;
public ExternalApiService() {
this.restClient = RestClient.create();
}
public Map<String, Object> getUser(String userId) {
return restClient.get()
.uri("https://api.example.com/users/{id}", userId)
.retrieve()
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
}
}
2. POST Request with Body
Before (RestTemplate)
java
public void sendNotification(String userId, String message) {
String url = baseUrl + "/notifications";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
Map<String, String> payload = Map.of(
"userId", userId,
"message", message
);
HttpEntity<Map<String, String>> request = new HttpEntity<>(payload, headers);
restTemplate.postForEntity(url, request, Void.class);
}
After (RestClient)
java
public void sendNotification(String userId, String message) {
Map<String, String> payload = Map.of(
"userId", userId,
"message", message
);
restClient.post()
.uri(baseUrl + "/notifications")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(payload)
.retrieve()
.toBodilessEntity();
}
3. Exchange with Custom Headers
Before (RestTemplate)
java
public Map<String, Object> enrichUserProfile(String userId) {
String url = baseUrl + "/users/" + userId + "/profile";
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
HttpEntity<?> request = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(
url,
HttpMethod.GET,
request,
Map.class
);
return response.getBody();
}
After (RestClient)
java
public Map<String, Object> enrichUserProfile(String userId) {
return restClient.get()
.uri(baseUrl + "/users/{id}/profile", userId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
}
4. DELETE Request
Before (RestTemplate)
java
public boolean requestDataDeletion(String userId) {
try {
String url = baseUrl + "/users/" + userId + "/data";
restTemplate.delete(url);
return true;
} catch (Exception e) {
return false;
}
}
After (RestClient)
java
public boolean requestDataDeletion(String userId) {
try {
restClient.delete()
.uri(baseUrl + "/users/{id}/data", userId)
.retrieve()
.toBodilessEntity();
return true;
} catch (Exception e) {
return false;
}
}
RestClient Configuration
Creating a Configured RestClient
java
@Configuration
public class RestClientConfig {
@Value("${external.api.base-url}")
private String baseUrl;
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
Using the Configured RestClient
java
@Service
public class ExternalApiService {
private final RestClient restClient;
public ExternalApiService(RestClient restClient) {
this.restClient = restClient;
}
// Methods can now use relative URIs
public Map<String, Object> getUser(String userId) {
return restClient.get()
.uri("/users/{id}", userId)
.retrieve()
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
}
}
Error Handling
RestClient Status Handlers
java
public Map<String, Object> getUserWithErrorHandling(String userId) {
return restClient.get()
.uri("/users/{id}", userId)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new UserNotFoundException("User not found: " + userId);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new ExternalServiceException("External service error");
})
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
}
Type-Safe Responses
Using ParameterizedTypeReference
java
// For generic types like Map or List
Map<String, Object> map = restClient.get()
.uri("/data")
.retrieve()
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
Direct Class Mapping
java
// For simple types
User user = restClient.get()
.uri("/users/{id}", userId)
.retrieve()
.body(User.class);
String text = restClient.get()
.uri("/text")
.retrieve()
.body(String.class);
Complete Service Migration Example
Before
java
@Service
public class ExternalApiService {
private final RestTemplate restTemplate;
@Value("${external.api.base-url}")
private String baseUrl;
public ExternalApiService() {
this.restTemplate = new RestTemplate();
}
public boolean verifyEmail(String email) {
try {
String url = baseUrl + "/verify/email?email=" + email;
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
return Boolean.TRUE.equals(response.getBody().get("valid"));
} catch (Exception e) {
return false;
}
}
}
After
java
@Service
public class ExternalApiService {
private final RestClient restClient;
@Value("${external.api.base-url}")
private String baseUrl;
public ExternalApiService() {
this.restClient = RestClient.create();
}
public boolean verifyEmail(String email) {
try {
Map<String, Object> response = restClient.get()
.uri(baseUrl + "/verify/email?email={email}", email)
.retrieve()
.body(new ParameterizedTypeReference<Map<String, Object>>() {});
return response != null && Boolean.TRUE.equals(response.get("valid"));
} catch (Exception e) {
return false;
}
}
}
WebClient Alternative
For reactive applications, use WebClient instead:
java
// WebClient for reactive/async operations
WebClient webClient = WebClient.create(baseUrl);
Mono<User> userMono = webClient.get()
.uri("/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);
RestClient is preferred for synchronous operations in non-reactive applications.