Agent Skills: RestTemplate to RestClient Migration Skill

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.

UncategorizedID: benchflow-ai/skillsbench/restclient-migration

Install this agent skill to your local

pnpm dlx add-skill https://github.com/benchflow-ai/skillsbench/tree/HEAD/tasks/spring-boot-jakarta-migration/environment/skills/restclient-migration

Skill Files

Browse the full folder contents for restclient-migration.

Download Skill

Loading file tree…

tasks/spring-boot-jakarta-migration/environment/skills/restclient-migration/SKILL.md

Skill Metadata

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)

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