home / skills / benchflow-ai / skillsbench / 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-migration

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.3 KB
---
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.

Overview

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.

How this skill works

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 to use it

  • Updating legacy code that uses deprecated RestTemplate in Spring Boot 3.2+
  • Switching to a fluent, synchronous HTTP client with better generics support
  • Creating or refactoring RestClient beans with baseUrl and default headers
  • Migrating request patterns: GET/POST/DELETE and custom-header exchanges
  • Implementing clear, status-based error handling for external HTTP calls

Best practices

  • Register a single configured RestClient bean (baseUrl, default headers) and inject it where needed
  • Prefer ParameterizedTypeReference for generic responses (List, Map) to preserve type safety
  • Use onStatus handlers to map 4xx/5xx responses to domain exceptions instead of catching generic exceptions
  • Use retrieve().toBodilessEntity() for endpoints returning no body to preserve intent
  • Avoid building full absolute URIs in services; use baseUrl in the RestClient and relative URIs for clarity

Example use cases

  • Convert restTemplate.getForEntity(...) returning Map to restClient.get().uri(...).retrieve().body(new ParameterizedTypeReference<...>() {})
  • Replace restTemplate.postForEntity with restClient.post().uri(...).contentType(...).body(payload).retrieve().toBodilessEntity()
  • Migrate delete calls that previously caught exceptions to restClient.delete().uri(...).retrieve().toBodilessEntity() with try/catch around the call
  • Configure a RestClient bean in a @Configuration class with baseUrl and default headers for all downstream services
  • Handle 404 vs 5xx differently by using onStatus predicates to throw UserNotFoundException or ExternalServiceException

FAQ

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