Skillsbench restclient-migration
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.
install
source · Clone the upstream repo
git clone https://github.com/benchflow-ai/skillsbench
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/benchflow-ai/skillsbench "$T" && mkdir -p ~/.claude/skills && cp -r "$T/tasks/spring-boot-jakarta-migration/environment/skills/restclient-migration" ~/.claude/skills/benchflow-ai-skillsbench-restclient-migration && rm -rf "$T"
manifest:
tasks/spring-boot-jakarta-migration/environment/skills/restclient-migration/SKILL.mdsource content
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)
@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)
@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)
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)
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)
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)
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)
public boolean requestDataDeletion(String userId) { try { String url = baseUrl + "/users/" + userId + "/data"; restTemplate.delete(url); return true; } catch (Exception e) { return false; } }
After (RestClient)
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
@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
@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
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
// 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
// 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
@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
@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:
// 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.