Open In App

A Guide to RestClient in Spring Boot

Last Updated : 26 Sep, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

In Spring Boot applications, external services often need to be communicated via REST APIs. Traditionally, RestTemplate was used for this purpose, but it is now considered a legacy approach. Starting from Spring Framework 6.1 and Spring Boot 3.2, RestClient has been introduced as a modern alternative.

RestClient provides a fluent and flexible API, supporting synchronous and asynchronous HTTP requests in a Spring Boot application. It focuses on cleaner API design with features such as error handling, customization, and support for modern web standards like HTTP/2.

What is RestClient?

RestClient is a new API for making HTTP requests in Spring Boot, simplifying HTTP communications with a streamlined, developer-friendly interface. We can easily configure and execute requests using its builder pattern, improving code readability and maintainability.

Key Concepts of RestClient in Spring Boot

1. Fluent API

One of the main features of RestClient is its fluent API design. This allows developers to build requests in a readable, chainable manner, reducing boilerplate code.

Example:

RestClient restClient = RestClient.builder().build();

String response = restClient.get()
.uri("https://api.example.com/resource")
.retrieve()
.body(String.class);

In this example:

  • .get(): Initiates an HTTP GET request.
  • .uri(): Sets the target URI.
  • .retrieve(): Executes the request.
  • .body(String.class): Extracts the response as a String.

The fluent API makes it easy to understand the sequence of the operations, and the method chaining makes the code cleaner and more expressive.

2. Synchronous and Asynchronous Requests

RestClient supports both synchronous (blocking) and asynchronous (non-blocking) HTTP calls.

Synchronous Request Example:

String response = restClient.get()
.uri("https://api.example.com/data")
.retrieve()
.body(String.class);

Here, the method waits for the response, blocking the calling thread until it arrives.

Asynchronous Request

For non-blocking scenarios, RestClient returns the CompletableFuture or the reactive type like Mono (if using the Project Reactor) for the handling asynchronous requests.

import java.util.concurrent.CompletableFuture;

CompletableFuture<String> responseFuture = restClient.get()
.uri("https://api.example.com/data")
.retrieve()
.bodyAsync(String.class);

responseFuture.thenAccept(response -> {
System.out.println("Async Response: " + response);
});

In this example,

bodyAsync(String.class) returns a CompletableFuture<String>, allowing the application to continue execution without blocking.

3. Error Handling and Status Code Handling

RestClient provides the built-in support for the error handling and HTTP status code management. Developers can easily filter and handle the different HTTP response statuses, ensuring that the errors are handled gracefully.

Basic Error Handling:

RestClient allows us to handle the errors based on the HTTP status codes by using the onStatus() method. It can be useful for catching the specific errors like client-side(4xx) or server-side (5xx) failures.

restClient.get()
.uri("https://api.example.com/resource")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
// Handle 4xx errors (client-side errors)
return Mono.error(new RuntimeException("Client error"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
// Handle 5xx errors (server-side errors)
return Mono.error(new RuntimeException("Server error"));
})
.body(String.class);

Here,

  • onStatus(): It can be used to filter responses based on the HTTP status codes.
  • HttpStatus::is4xxClientError: It can handle the 4xx errors (e.g., 404 Not Found, 400 Bad Request).
  • HttpStatus::is5xxServerError: It can handle the 5xx errors (e.g., 500 Internal Server Error).

Handling the Specific Status Codes:

We can also handle the specific status code to provide the more detailed error messages or responses.

restClient.get()
.uri("https://api.example.com/resource")
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND, response -> {
return Mono.error(new RuntimeException("Resource not found"));
})
.onStatus(status -> status == HttpStatus.INTERNAL_SERVER_ERROR, response -> {
return Mono.error(new RuntimeException("Internal server error"));
})
.body(String.class);

This approach allows us to create the custom handling logic for the specific status codes, it ensures that the application can respond appropriately to different error scenarios.

4. Request Customization

RestClient provides several ways to customize the HTTP requests, such as adding the headers, query parameters, or path variables. This flexibility allows the developers to meet specific API requirements easily.

Adding Custom Headers:

Header are the crucial part of the HTTP requests, often used for the authentication (eg: JWT tokens) or providing the additional context (eg: content type).

restClient.get()
.uri("https://api.example.com/resource")
.header("Authorization", "Bearer my-token")
.header("Content-Type", "application/json")
.retrieve()
.body(String.class);

In this example,

  • header() can be used to add the custom HTTP headers to the request.
  • Multiple headers can be added for the different purposes (e.g., authentication and content type).

Path Variables and Query Parameters:

For the RESTful services that requires the path variables or query parameters, RestClient provides the methods to easily inject these values into the URI.

String response = restClient.get()
.uri("https://api.example.com/resource/{id}", 123) // Path variable
.queryParam("filter", "active") // Query parameter
.retrieve()
.body(String.class);

In this example:

  • {id} is the path variable, replaced by the value 123.
  • queryParam("filter", "active") adds the query parameter (?filter=active) to the request.

5. Body Handling

RestClient provides the methods for sending and receiving the request bodies in different formats (e.g., JSON, XML, String, or custom objects).

Sending Request Body

For the methods like POST, PUT, and PATCH, we can include the request body to be sent to the server.

User user = new User("John", "Doe");

String response = restClient.post()
.uri("https://api.example.com/users")
.body(user) // Send custom object as JSON
.retrieve()
.body(String.class);

Here,

body(user) sends the user object in the request. Spring can automatically serializes the Java object into the JSON using Jackson (assuming the necessary dependencies are included).

Receiving Response Body

We can receive the response body in the various formats, including as the custom object, list, or raw string.

User userResponse = restClient.get()
.uri("https://api.example.com/user/123")
.retrieve()
.body(User.class); // Parse response into custom object

In this case, the response can be de-serialized into the User object.

6. Configuring the Timeout

When making the HTTP requests, we may want to configure the timeouts to avoid waiting indefinitely for the response. It can be done by configuring the underlying HTTP client (HttpClient in Java 11+) used by the RestClient.

import java.time.Duration;
import java.net.http.HttpClient;

HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5)) // Set connection timeout
.build();

RestClient restClient = RestClient.builder()
.httpClient(httpClient) // Configure RestClient to use this HttpClient
.build();

Here,

The connectTimeout(Duration.ofSeconds(5)) sets the connection timeout of 5 seconds. If the connection isn’t established within this period, an exception will be thrown.

7. Interceptors and Filters

RestClient supports the interceptors and filter to modify the requests and responses. It can be useful for the tasks like logging, adding the authentication tokens, or performing the request validations.

Example of Adding an Interceptor for Logging:

@Bean
public RestClient restClient(RestClientBuilder builder) {
return builder.interceptor((request, next) -> {
System.out.println("Request URI: " + request.getUri());
return next.exchange(request);
}).build();
}

In this example,

The interceptor logs the request URI before the request is sent. We can also modify the request or response within the interceptor.

8. Built-in Support for the HTTP/2

RestClient supports the modern HTTP standards such as HTTP/2. We can enable this by using the Java's HttpClient, which supports the HTTP/2 out of the box.

HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // Enable HTTP/2
.build();

RestClient restClient = RestClient.builder()
.httpClient(httpClient)
.build();

With HTTP/2, we can take the advantages of faster, more efficient communication between the services, especially in the microservices environments.

Setting up the Project

To use RestClient, you need Spring Boot 3.2 or later. Add the following Maven dependency.

Maven Dependency:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Make sure the spring-boot-starter-web is compatible with Spring Boot 3.2 or later, as the RestClient API is part of the Spring Framework 6.1.

Creating a RestClient Instance

The RestClient instance can be created in the two ways:

  1. Through the dependency injection by registering it as the Spring bean.
  2. By creating it manually in the service or controller class.

Using the Spring Configuration to Create a Bean

The best pratice in the Spring Boot application is to create the RestClient bean that can be reused across the different components.

import org.springframework.boot.web.client.RestClientBuilder;
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(RestClientBuilder builder) {
return builder.build(); // Create a RestClient instance
}
}

Here, we have used the RestClientBuilder to build the RestClient instance and register it as the Spring bean. We can inject this bean into any service or controller where you need it.

Manually Creating the RestCleint in a Class

import org.springframework.web.client.RestClient;

public class ApiService {

private final RestClient restClient;

public ApiService() {
this.restClient = RestClient.builder().build(); // Create manually
}

// other methods
}

In this case, RestClient can be manually built using the builder() method. However, using the Spring bean approach is recommended for the better configuration and reuse.

Performing the HTTP requests with RestClient

Now we will explore how to use the RestClient to make the different types of the HTTP requests such as GET, POST, PUT, DELETE etc. We will also request customization, handling the response bodies, and error handling.

Making the simple GET Request

The Get requests is one of the most commonly used the HTTP methods. Here is how to perform the GET request using the RestClient:

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class ApiService {

private final RestClient restClient;

public ApiService(RestClient restClient) {
this.restClient = restClient;
}

public String getApiResponse(String url) {
return restClient.get() // Specify that this is a GET request
.uri(url) // Set the URI for the request
.retrieve() // Execute the request and retrieve the response
.body(String.class); // Extract response body as String
}
}

Explanation:

  • The restClient.get() method indicates the GET request.
  • uri(url) sets the endpoint.
  • retrieve() executes the request and retrieves the response.
  • body(String.class) parses the response as the String.

Making the Post Request

The post request can be used to send the data to the server to create or update the resources.

Here the example:

import org.springframework.stereotype.Service;

@Service
public class ApiService {

private final RestClient restClient;

public ApiService(RestClient restClient) {
this.restClient = restClient;
}

public String sendPostRequest(String url, Object requestBody) {
return restClient.post() // Specify POST method
.uri(url) // Set the URL
.body(requestBody) // Send the request body
.retrieve() // Execute and get response
.body(String.class); // Extract the response body as String
}
}

In this example,

  • We have used the restClient.post() to indicate the POST request.
  • The body(requestBody) method sends the request data to the server.

Handling the Response and Errors

Handling Responses:

The RestClient allows us to extract the response bodies in the various formats, such as String, Object, or List.

public MyResponseClass getCustomResponse(String url) {
return restClient.get()
.uri(url)
.retrieve()
.body(MyResponseClass.class); // Parse response into a custom class
}

Here, the response can be parsed into the custom Java object MyResponseClass using the body().

Handling Errors:

We can handle the errors by checking the HTTP status codes using the onStatus() method. It is especially useful for the catching client-side and server-side errors.

import org.springframework.http.HttpStatus;

public String getApiResponseWithErrorHandling(String url) {
return restClient.get()
.uri(url)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Client error occurred")))
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new RuntimeException("Server error occurred")))
.body(String.class);
}

In this example,

  • onStatus() checks for the 4xx and 5xx HTTP status codes and handling the errors accordingly.
  • If the error occurs, the Mono.error() returned, indicating the error.

Asynchronous Requests with RestClient

RestClient also supports the non-blocking, asynchronous HTTP requests, which can be useful when you don't want to block the application's execution while waiting for the response.

Here’s an example using CompletableFuture for asynchronous execution:

import java.util.concurrent.CompletableFuture;

public CompletableFuture<String> getAsyncResponse(String url) {
return restClient.get()
.uri(url)
.retrieve()
.bodyAsync(String.class); // Fetch response asynchronously
}

This method returns the CompletableFuture<String>, which can be processed asynchronously.

Conclusion

In conclusion, RestClient offers a modern, flexible, and powerful API for handling HTTP requests in Spring Boot applications. It improves upon RestTemplate with a fluent API, built-in error handling, support for synchronous and asynchronous requests, and various customization options.


Next Article

Similar Reads