Introduction
Spring Framework 6.1 introduced RestClient
, a modern and fluent API for synchronous HTTP communication that simplifies working with REST APIs. If you’ve been using RestTemplate
for years, RestClient
will feel familiar yet refreshingly easier to work with.
This guide will walk you through everything you need to know about RestClient
, from basic HTTP requests to advanced features like authentication, error handling, and declarative HTTP interfaces.
All code examples in this guide are available in the accompanying GitHub repository: https://github.com/theautonomy/restclient-example.git |
See this git repository for example of using different underlying client request factories (via http client library) for RestClient: https://github.com/theautonomy/spring-http-client-request-factories-example.git |
Why RestClient?
Traditional RestTemplate
has served us well, but RestClient
offers several improvements:
-
Fluent API: Chain method calls for more readable code
-
Simplified configuration: Easier setup with builder pattern
-
Better error handling: More intuitive exception management
-
Modern Java support: Takes advantage of recent Java features
-
HTTP Interface support: Declarative REST clients with annotations
Getting Started
Dependencies
First, add Spring Web to your project. For Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.6</version>
</dependency>
For Gradle:
implementation 'org.springframework.boot:spring-boot-starter-web:3.5.6'
Basic Configuration
You can create a RestClient
in several ways. The simplest approach:
RestClient restClient = RestClient.create();
For more control, use the builder pattern:
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("User-Agent", "MyApp/1.0")
.defaultHeader("Accept", "application/json")
.build();
In Spring Boot applications, it’s best to configure RestClient
as beans:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("User-Agent", "MyApp/1.0")
.defaultHeader("Accept", "application/json")
.build();
}
}
Using Spring Boot’s auto-configuration, you can inject RestClient.Builder directly and customize it per use case.
|
Basic Operations
Simple GET Requests
Let’s start with the most common operation: fetching data.
@Service
public class UserService {
private final RestClient restClient;
public UserService(RestClient restClient) {
this.restClient = restClient;
}
public User getUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
public List<User> getAllUsers() {
return restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
}
Notice how clean this is compared to RestTemplate
. No manual URL construction or type casting needed!
GET with Query Parameters
Adding query parameters is straightforward:
public List<User> searchUsers(String name, Integer minAge) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/users/search")
.queryParam("name", name)
.queryParam("minAge", minAge)
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
Or using string formatting:
public List<User> searchUsers(String name, Integer age) {
return restClient.get()
.uri("/users/search?name={name}&age={age}", name, age)
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
POST Requests
Creating resources with POST is just as elegant:
public User createUser(User user) {
return restClient.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.body(user)
.retrieve()
.body(User.class);
}
For form data submissions:
public User submitForm(String name, String email) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("name", name);
formData.add("email", email);
return restClient.post()
.uri("/form")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(formData)
.retrieve()
.body(User.class);
}
PUT and PATCH Requests
Updates follow the same pattern:
public User updateUser(Long id, User user) {
return restClient.put()
.uri("/users/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(user)
.retrieve()
.body(User.class);
}
public User partialUpdateUser(Long id, Map<String, Object> updates) {
return restClient.patch()
.uri("/users/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(updates)
.retrieve()
.body(User.class);
}
DELETE Requests
Deleting resources is simple:
public void deleteUser(Long id) {
restClient.delete()
.uri("/users/{id}", id)
.retrieve()
.toBodilessEntity();
}
public int deleteUser(Long id) {
ResponseEntity<Void> response = restClient.delete()
.uri("/users/{id}", id)
.retrieve()
.toBodilessEntity();
return response.getStatusCode().value();
}
Advanced Features
Custom Headers
Adding custom headers to requests:
public User getUserWithHeaders(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.header("X-Request-ID", UUID.randomUUID().toString())
.header("X-API-Version", "v2")
.retrieve()
.body(User.class);
}
Using a header consumer for complex scenarios:
public User getUserWithDynamicHeaders(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.headers(headers -> {
headers.set("X-Request-ID", UUID.randomUUID().toString());
headers.set("X-Timestamp", Instant.now().toString());
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
})
.retrieve()
.body(User.class);
}
Authentication
Basic authentication:
public User authenticatedRequest(String username, String password) {
return restClient.get()
.uri("/secure/user")
.headers(headers -> headers.setBasicAuth(username, password))
.retrieve()
.body(User.class);
}
For application-wide authentication, configure it in the builder:
@Bean
public RestClient authenticatedRestClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer " + getAccessToken())
.build();
}
Bearer token authentication:
Spring Security 6.4 has added RestClient support for OAuth2 |
Response Entity Access
Sometimes you need access to status codes and headers:
public ResponseEntity<User> getUserWithMetadata(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
}
public void processUserResponse(Long id) {
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.toEntity(User.class);
System.out.println("Status: " + response.getStatusCode());
System.out.println("Headers: " + response.getHeaders());
System.out.println("Body: " + response.getBody());
}
Error Handling
RestClient provides several ways to handle errors:
public User getUserWithErrorHandling(Long id) {
try {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
} catch (RestClientResponseException e) {
System.err.println("Error status: " + e.getStatusCode());
System.err.println("Error body: " + e.getResponseBodyAsString());
throw new UserNotFoundException("User not found with id: " + id);
}
}
Custom error handling with status handlers:
public User getUserWithCustomErrorHandling(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new UserNotFoundException(
"User not found: " + response.getStatusCode());
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new ServiceUnavailableException(
"Service error: " + response.getStatusCode());
})
.body(User.class);
}
Request Interceptors
Interceptors allow you to modify requests and responses:
@Bean
public RestClient restClientWithLogging(RestClient.Builder builder) {
return builder
.baseUrl("https://api.example.com")
.requestInterceptor(loggingInterceptor())
.build();
}
private ClientHttpRequestInterceptor loggingInterceptor() {
return (request, body, execution) -> {
System.out.println("Request: " + request.getMethod() + " " +
request.getURI());
System.out.println("Headers: " + request.getHeaders());
if (body.length > 0) {
System.out.println("Body: " + new String(body));
}
ClientHttpResponse response = execution.execute(request, body);
System.out.println("Response Status: " + response.getStatusCode());
System.out.println("Response Headers: " + response.getHeaders());
return response;
};
}
HTTP Interface: Declarative REST Clients
One of the most powerful features of RestClient
is support for declarative HTTP interfaces using @HttpExchange
annotations.
Defining an HTTP Interface
Create an interface with annotated methods:
import org.springframework.web.service.annotation.*;
@HttpExchange
public interface UserClient {
@GetExchange("/users")
List<User> getAllUsers();
@GetExchange("/users/{id}")
User getUser(@PathVariable Long id);
@PostExchange("/users")
User createUser(@RequestBody User user);
@PutExchange("/users/{id}")
User updateUser(@PathVariable Long id, @RequestBody User user);
@DeleteExchange("/users/{id}")
void deleteUser(@PathVariable Long id);
@GetExchange("/users/search")
List<User> searchUsers(@RequestParam String name,
@RequestParam(required = false) Integer age);
}
Configuring HTTP Interface Beans
Create a bean for the HTTP interface:
@Configuration
public class HttpInterfaceConfig {
@Bean
public UserClient userClient(RestClient.Builder builder) {
RestClient restClient = builder
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.build();
return factory.createClient(UserClient.class);
}
}
Using HTTP Interfaces
Inject and use the interface like any other Spring bean:
@Service
public class UserService {
private final UserClient userClient;
public UserService(UserClient userClient) {
this.userClient = userClient;
}
public void demonstrateHttpInterface() {
// Simple GET
List<User> users = userClient.getAllUsers();
// GET with path variable
User user = userClient.getUser(1L);
// POST
User newUser = new User("John", "john@example.com", 30);
User created = userClient.createUser(newUser);
// PUT
created.setEmail("john.doe@example.com");
User updated = userClient.updateUser(created.getId(), created);
// DELETE
userClient.deleteUser(created.getId());
// Search with query parameters
List<User> results = userClient.searchUsers("John", 25);
}
}
The HTTP interface approach eliminates boilerplate code and makes your REST client code cleaner and more maintainable.
Best Practices
Reuse RestClient Instances
RestClient
instances are thread-safe and should be reused:
// Good - single RestClient instance
@Service
public class ApiService {
private final RestClient restClient;
public ApiService(RestClient restClient) {
this.restClient = restClient;
}
}
// Avoid - creating new instances repeatedly
public void getUser() {
RestClient client = RestClient.create(); // Don't do this in methods!
// ...
}
Use Meaningful Base URLs
Configure base URLs in your beans for clarity:
@Configuration
public class RestClientConfig {
@Bean
public RestClient githubClient(RestClient.Builder builder) {
return builder
.baseUrl("https://api.github.com")
.defaultHeader("Accept", "application/vnd.github.v3+json")
.build();
}
@Bean
public RestClient internalApiClient(RestClient.Builder builder) {
return builder
.baseUrl("http://internal-api:8080")
.defaultHeader("X-Service-Name", "user-service")
.build();
}
}
Handle Timeouts
Configure appropriate timeouts for your REST calls:
@Bean
public RestClient restClientWithTimeouts(RestClient.Builder builder) {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5 seconds
factory.setConnectionRequestTimeout(5000);
return builder
.baseUrl("https://api.example.com")
.requestFactory(factory)
.build();
}
Use ParameterizedTypeReference for Collections
When working with generic types like collections:
// Good - using ParameterizedTypeReference
public List<User> getUsers() {
return restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
}
// Won't work - type erasure issues
public List<User> getUsers() {
return restClient.get()
.uri("/users")
.retrieve()
.body(List.class); // Returns List<LinkedHashMap>, not List<User>
}
Use Records for Response DTOs
Java records are perfect for HTTP response objects:
public record ApiResponse<T>(
int status,
String message,
T data
) {}
public ApiResponse<List<User>> getUsers() {
return restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<ApiResponse<List<User>>>() {});
}
Migration from RestTemplate
If you’re migrating from RestTemplate
, here’s a comparison:
RestTemplate |
---|
|
|
|
RestClient |
---|
|
|
|
Due to the limitation of template style API, Spring team recommends starting to migrate from RestClient to RestClient . Check out this excellent post: The state of HTTP clients in Spring
|
Common Pitfalls
Forgetting to Call Terminal Operations
// Wrong - request never executes!
restClient.get()
.uri("/users")
.retrieve();
// Correct - must call a terminal operation like body(), toEntity(), etc.
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
Not Handling Generic Types Properly
// Wrong - loses generic type information
List users = restClient.get()
.uri("/users")
.retrieve()
.body(List.class); // Returns List<LinkedHashMap>
// Correct - preserves type information
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});
Ignoring Error Responses
// Risky - doesn't handle errors
public User getUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class); // Throws exception on 4xx/5xx
}
// Better - explicit error handling
public Optional<User> getUser(Long id) {
try {
User user = restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
return Optional.ofNullable(user);
} catch (HttpClientErrorException.NotFound e) {
return Optional.empty();
}
}
Creating RestClient Instances Unnecessarily
// Wrong - creates new instance for each call
public class UserService {
public User getUser(Long id) {
RestClient client = RestClient.create();
return client.get()
.uri("https://api.example.com/users/{id}", id)
.retrieve()
.body(User.class);
}
}
// Correct - reuse single instance
@Service
public class UserService {
private final RestClient restClient;
public GoodService(RestClient restClient) {
this.restClient = restClient;
}
public User getUser(Long id) {
return restClient.get()
.uri("/users/{id}", id)
.retrieve()
.body(User.class);
}
}
Conclusion
RestClient
brings a modern, fluent API to Spring’s HTTP client capabilities that makes REST API communication code cleaner and more maintainable. Its main advantages are:
-
Simplified syntax with method chaining
-
Better error handling capabilities
-
Support for declarative HTTP interfaces
-
Seamless integration with Spring Boot
-
Improved developer experience over RestTemplate
Whether you’re starting a new project or maintaining existing code, RestClient
is worth considering for your HTTP communication needs. It provides all the power of RestTemplate
with a more developer-friendly API, plus modern features like HTTP interfaces that can significantly reduce boilerplate code.
About the Author
Wei Li is a passionate software developer and technical writer specializing in Java and Spring Framework. With years of experience in building enterprise applications, Wei Li enjoys sharing knowledge through detailed tutorials and practical guides. For questions or feedback, feel free to reach out at weili.mail@gmail.com. |