home / skills / benchflow-ai / skillsbench / restclient-migration
/tasks/spring-boot-jakarta-migration/environment/skills/restclient-migration
This skill helps migrate RestTemplate to RestClient in Spring Boot 3.2+, enabling fluent HTTP calls, type-safe responses, and simplified error handling.
npx playbooks add skill benchflow-ai/skillsbench --skill restclient-migrationReview the files below or copy the command above to add this skill to your agents.
---
name: restclient-migration
description: Migrate RestTemplate to RestClient in Spring Boot 3.2+. Use when replacing deprecated RestTemplate with modern fluent API, updating HTTP client code, or configuring RestClient beans. Covers GET/POST/DELETE migrations, error handling, and ParameterizedTypeReference usage.
---
# 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.
This skill guides migrating Spring Boot synchronous HTTP code from RestTemplate to the modern RestClient introduced in Spring Framework 6.1 (Spring Boot 3.2+). It focuses on practical, line-by-line conversions for GET, POST, DELETE and exchange patterns, shows how to configure RestClient beans, and explains error handling and type-safe response handling with ParameterizedTypeReference.
The skill inspects typical RestTemplate usage patterns and maps them to equivalent RestClient fluent calls. It demonstrates building a configured RestClient, converting request construction (headers, body, URIs), handling responses with typed bodies or ParameterizedTypeReference, and replacing ResponseErrorHandler with onStatus handlers for status-based error handling. Examples include direct class mapping, generic type mapping, and bodiless responses.
When should I still use WebClient instead of RestClient?
Use WebClient for reactive or asynchronous applications. RestClient is the recommended synchronous API for non-reactive code.
How do I map generic response types safely?
Use new ParameterizedTypeReference<T>() {} with retrieve().body(...) to retain full generic type information like List<User> or Map<String,Object>.