Table of Contents
- Introduction to RestClient
- Setting Up RestClient in Spring Boot
- Making HTTP Requests with RestClient
- Handling Responses in RestClient
- Customizing RestClient
- Authentication with RestClient
- Advanced Usage of RestClient
- Testing with RestClient
1. Introduction to RestClient
In Spring Boot 3.2, a new HTTP client called RestClient
was released as a RestClient
is a new HTTP client introduced in Spring Boot 3.2 as a modern alternative for RestTemplate
. By offering a more declarative and fluent API while maintaining synchronous behavior, it makes submitting HTTP requests easier.
It brings a balance between RestTemplate
and WebClient
and is simple to use, test, and configure because it is built on top of HttpInterface
.
RestClient
is a new HTTP client introduced in Spring Boot 3.2 as a modern alternative to RestTemplate
. It simplifies making HTTP requests by providing a more fluent and declarative API while retaining synchronous behavior.
It is built on top of HttpInterface
, which makes it easy to use, test, and configure, bringing a balance between RestTemplate
and WebClient
.
Why Use RestClient Over RestTemplate/WebClient?
Feature | RestClient | RestTemplate | WebClient |
---|---|---|---|
Introduced In | Spring Boot 3.2 | Spring 3.0 | Spring WebFlux |
API Style | Fluent & Declarative | Verbose & Imperative | Reactive |
Blocking/Synchronous | ✅ Yes | ✅ Yes | ❌ No (Non-blocking) |
Async Support | ❌ No | ❌ No | ✅ Yes |
Streaming Support | ✅ Yes | ❌ No | ✅ Yes |
Ease of Use | ✅ Simple & Modern | ❌ Verbose | ⚠️ Complex for Simple Use Cases |
Recommended For | Modern synchronous applications | Legacy support | Reactive applications |
Why use RestClient
instead of RestTemplate
?
RestTemplate
is now in maintenance mode and will be deprecated in the future.RestClient
provides a fluent API that improves readability and usability.- It allows better integration with error handling, authentication, and interceptors.
Why use RestClient
instead of WebClient
?
RestClient
is synchronous, making it easier to use in traditional Spring MVC applications.WebClient
is asynchronous and non-blocking, which is better suited for reactive applications.- If you don’t need reactive programming,
RestClient
provides better simplicity and ease of use.
Features & Benefits of RestClient
☑️ Fluent & Readable API – Allows writing concise, readable, and chainable request calls.
☑️ Supports Serialization/Deserialization – Automatically maps JSON responses to Java objects.
☑️ Error Handling Built-in – Supports custom exception handling for different HTTP status codes.
☑️ Easier Configuration – Configurable with authentication, timeouts, and interceptors.
☑️ Supports Streaming – Handles large responses efficiently.
☑️ Modern and Future-Proof – Designed for newer Spring Boot versions with better maintainability
2. Setting Up RestClient in Spring Boot
To use RestClient
in a Spring Boot 3.2+ application, you need to set up the necessary dependencies and configure a RestClient
bean. This section covers both steps in detail.
Adding Dependencies (Spring Boot 3.2+ Required)
Starting from Spring Boot 3.2, RestClient
is included in the spring-boot-starter-web
and spring-boot-starter-webflux
dependencies.
Maven Dependency
If your project already has spring-boot-starter-web
, you don’t need to add anything extra.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
If you're using Spring WebFlux, RestClient
is also included:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Configuring RestClient Bean
To use RestClient
throughout your application, you can create a global RestClient
bean in a configuration class.
1. Basic RestClient Bean Configuration
The simplest way to configure RestClient
is by creating a bean using RestClient.create()
.
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() {
return RestClient.create(); // Creates a default RestClient instance
}
}
- This
RestClient
bean can now be injected and used across the application. - It uses default settings and does not have a predefined base URL.
2. Configuring RestClient with a Base URL
If you frequently call the same API, it's better to configure a base URL in the bean.
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com") // Base URL for all requests
.build();
}
- Now, all requests made using this
RestClient
will usehttps://jsonplaceholder.typicode.com
as the base.
3. Customizing RestClient with Headers, Timeouts, and Interceptors
You can configure RestClient
with default headers, timeouts, and interceptors to customize its behavior.
import java.time.Duration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer your_token") // Set Authorization header
.defaultHeader("Content-Type", "application/json") // Set default Content-Type
.requestFactory(new JdkClientHttpRequestFactory()) // Use a custom HTTP request factory
.build();
}
Key Customizations:
☑️ Base URL – Defines a global API URL for requests.
☑️ Headers – Automatically adds default headers (e.g., Authorization).
☑️ Timeouts – You can set timeout values using a request factory.
☑️ Interceptors – Useful for logging, modifying requests, or handling authentication.
Injecting and Using RestClient in a Service
Once you’ve configured the RestClient
bean, you can inject it into your services and use it to make HTTP calls.
Example: Using RestClient for a GET Request
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 getData() {
return restClient.get()
.uri("/posts/1") // This will use the base URL from the config
.retrieve()
.body(String.class);
}
}
What Happens Here?
RestClient
is injected into theApiService
class.- The
getData()
method sends a GET request tohttps://jsonplaceholder.typicode.com/posts/1
. - The response is automatically converted into a Java String (can be replaced with a DTO).
3. Making HTTP Requests with RestClient
Once RestClient
is configured, you can use it to make GET, POST, PUT, and DELETE HTTP requests. This section covers how to perform these operations with examples.
1. Making GET Requests
A GET request is used to fetch data from an API.
Example: Simple GET Request
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 getPostById(int id) {
return restClient.get()
.uri("/posts/{id}", id) // Uses path variable
.retrieve()
.body(String.class); // Returns response body as a String
}
}
Key Points:
☑️ restClient.get()
starts a GET request.
☑️ .uri("/posts/{id}", id)
dynamically replaces {id}
with the provided value.
☑️ .retrieve().body(String.class)
extracts the response body.
2. Making POST Requests
A POST request is used to send data to a server to create a new resource.
Example: Sending a JSON Request Body
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 createPost(Post post) {
return restClient.post()
.uri("/posts")
.body(post) // Sends the request body as JSON
.retrieve()
.body(String.class); // Returns the created resource
}
}
Post Model (DTO)
public class Post {
private String title;
private String body;
private int userId;
// Constructors, Getters, Setters
}
Key Points:
☑️ restClient.post()
starts a POST request.
☑️ .body(post)
automatically serializes the Java object into JSON.
☑️ The API will return the created resource as a response.
3. Making PUT Requests
A PUT request updates an existing resource.
Example: Updating a Post
public String updatePost(int id, Post post) {
return restClient.put()
.uri("/posts/{id}", id)
.body(post)
.retrieve()
.body(String.class);
}
Key Points:
☑️ restClient.put()
starts a PUT request.
☑️ .body(post)
sends the updated data as JSON.
☑️ The API updates the resource and returns the modified version.
4. Making DELETE Requests
A DELETE request removes a resource from the server.
Example: Deleting a Post
public void deletePost(int id) {
restClient.delete()
.uri("/posts/{id}", id)
.retrieve();
}
Key Points:
☑️ restClient.delete()
starts a DELETE request.
☑️ .uri("/posts/{id}", id)
specifies which resource to delete.
☑️ No response body is needed, so .body(String.class)
is omitted.
5. Handling Response Objects
Instead of using String.class
, you can deserialize JSON responses into Java objects.
Example: Using a DTO for GET Request
public Post getPostAsObject(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.body(Post.class); // Automatically maps JSON to Post object
}
- This returns a
Post
object instead of raw JSON. - Spring automatically maps the API response to the
Post
class.
4. Handling Responses in RestClient
When making HTTP requests with RestClient
, handling responses properly is crucial. This section covers:
- Deserializing JSON responses into Java objects.
- Handling different response types like
List<T>
and single objects. - Error handling and exception mapping for robust API communication.
1. Deserializing JSON Responses
RestClient
can automatically deserialize JSON responses into Java objects.
Example: Mapping JSON to a Java Object
Assume an API returns the following JSON:
{
"id": 1,
"title": "Spring Boot RestClient",
"body": "RestClient makes HTTP calls easier",
"userId": 100
}
Java DTO (Data Transfer Object) to Map JSON Response
public class Post {
private int id;
private String title;
private String body;
private int userId;
// Getters and setters
}
Using RestClient to Deserialize JSON to Post Object
public Post getPost(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.body(Post.class); // Converts JSON to Post object
}
☑️ Spring automatically maps JSON fields to the Java class properties using Jackson.
2. Handling Different Response Types
a) Handling a List of Objects (Multiple Items in Response)
If an API returns an array of objects, use List<T>
.
Example API Response (JSON Array):
[
{
"id": 1,
"title": "Post 1",
"body": "Body of post 1",
"userId": 100
},
{
"id": 2,
"title": "Post 2",
"body": "Body of post 2",
"userId": 101
}
]
Handling List Response in Java:
import java.util.List;
import org.springframework.core.ParameterizedTypeReference;
public List<Post> getAllPosts() {
return restClient.get()
.uri("/posts")
.retrieve()
.body(new ParameterizedTypeReference<List<Post>>() {});
}
☑️ Use ParameterizedTypeReference<List<Post>>
to handle generics properly.
b) Handling Different Response Types
Response Type | Usage |
---|---|
Single Object (T ) | restClient.get().body(Post.class) |
List of Objects (List<T> ) | restClient.get().body(new ParameterizedTypeReference<List<Post>>() {}) |
Raw JSON (String ) | restClient.get().body(String.class) |
Custom Response Wrapper (ResponseEntity<T> ) | restClient.get().body(ResponseEntity.class) |
3. Error Handling and Exception Mapping
When API calls fail (e.g., 404 Not Found, 500 Server Error, or timeouts), RestClient
can handle errors gracefully.
Basic Error Handling (Using onStatus
)
public Post getPostWithErrorHandling(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {
throw new RuntimeException("Failed to fetch post. Status: " + response.getStatusCode());
})
.body(Post.class);
}
☑️ onStatus(HttpStatusCode::isError, handler)
detects HTTP errors and throws custom exceptions.
Advanced Exception Mapping (Custom Exception Handling)
Instead of throwing a generic RuntimeException
, create custom exceptions based on the HTTP status code.
Custom Exception Classes
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
public class ApiException extends RuntimeException {
public ApiException(String message) {
super(message);
}
}
Handling Specific HTTP Errors
import org.springframework.http.HttpStatusCode;
public Post getPostWithCustomErrorHandling(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.onStatus(status -> status.value() == 404, (request, response) -> {
throw new NotFoundException("Post not found!");
})
.onStatus(HttpStatusCode::isError, (request, response) -> {
throw new ApiException("API Error: " + response.getStatusCode());
})
.body(Post.class);
}
☑️ Throws NotFoundException
for 404 errors and ApiException
for other HTTP errors.
4. Handling Empty Responses (204 No Content)
Some APIs return 204 No Content
when there is no response body. Handle this properly.
Example: Handling Empty Response Gracefully
public Optional<Post> getPostOptional(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.toEntity(Post.class)
.map(response -> response.getBody() != null ? Optional.of(response.getBody()) : Optional.empty());
}
☑️ Wraps the response in Optional<T>
to avoid NullPointerException
.
5. Customizing RestClient
To enhance API communication with RestClient
, customization is key. This section covers:
☑️ Setting custom headers (e.g., authentication, content type)
☑️ Configuring timeouts (preventing slow requests from blocking your app)
☑️ Using interceptors for logging requests and responses
1. Setting Custom Headers
Custom headers are often needed for authentication (e.g., Authorization header
) or content negotiation (e.g., Accept header
).
Example: Adding Headers Dynamically
public Post getPostWithHeaders(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.header("Authorization", "Bearer my-token") // Adds a custom header
.header("Accept", "application/json") // Specifies response format
.retrieve()
.body(Post.class);
}
☑️ .header(key, value)
allows adding headers dynamically per request.
Example: Using Headers for a POST Request
public String createPostWithHeaders(Post post) {
return restClient.post()
.uri("/posts")
.header("Authorization", "Bearer my-token")
.header("Content-Type", "application/json")
.body(post)
.retrieve()
.body(String.class);
}
☑️ For POST
requests, Content-Type
is essential when sending JSON data.
2. Configuring Timeouts
Time-sensitive APIs need timeouts to prevent slow responses from blocking your application.
Setting Timeouts in a RestClient Bean
import java.time.Duration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.stereotype.Component;
@Component
public class RestClientTimeoutCustomizer implements RestClientCustomizer {
@Override
public void customize(RestClient.Builder builder) {
builder.requestFactory(factory -> factory.setConnectTimeout(Duration.ofSeconds(5)) // Connection timeout
.setReadTimeout(Duration.ofSeconds(10))); // Read timeout
}
}
☑️ Connect timeout: Max time to establish a connection.
☑️ Read timeout: Max time to wait for a server response.
Using Timeouts in an Inline Request (Without Bean Customization)
import java.time.Duration;
public Post getPostWithTimeout(int id) {
return RestClient.builder()
.requestFactory(factory -> factory.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(8)))
.build()
.get()
.uri("/posts/{id}", id)
.retrieve()
.body(Post.class);
}
☑️ This inline approach is useful for temporary changes in timeout settings.
3. Interceptors and Logging Requests
Interceptors allow you to:
🔹 Log request and response data for debugging.
🔹 Add authentication tokens dynamically.
🔹 Modify or inspect requests before sending them.
Example: Custom Logging Interceptor
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
System.out.println("Request URI: " + request.getURI());
System.out.println("Request Method: " + request.getMethod());
System.out.println("Request Headers: " + request.getHeaders());
System.out.println("Request Body: " + new String(body, StandardCharsets.UTF_8));
ClientHttpResponse response = execution.execute(request, body);
System.out.println("Response Status: " + response.getStatusCode());
return response;
}
}
☑️ Logs request details (URI, headers, body) and response status.
Registering the Interceptor in RestClient
import org.springframework.stereotype.Component;
import org.springframework.boot.web.client.RestClientCustomizer;
@Component
public class RestClientInterceptorCustomizer implements RestClientCustomizer {
@Override
public void customize(RestClient.Builder builder) {
builder.requestInterceptor(new LoggingInterceptor()); // Adds custom logging
}
}
☑️ Spring Boot will automatically apply this interceptor to all RestClient
requests.
4. Customizing RestClient with Authentication
Many APIs require authentication via Bearer Tokens or Basic Auth.
Example: Adding Bearer Token to Every Request
import org.springframework.stereotype.Component;
import org.springframework.boot.web.client.RestClientCustomizer;
@Component
public class RestClientAuthCustomizer implements RestClientCustomizer {
@Override
public void customize(RestClient.Builder builder) {
builder.defaultHeader("Authorization", "Bearer my-secret-token"); // Applies to all requests
}
}
☑️ Now every request automatically includes the Authorization
header.
6. Authentication with RestClient
Authentication is crucial when working with external APIs. RestClient
supports multiple authentication methods, including:
☑️ Basic Authentication (username/password in headers)
☑️ Bearer Token Authentication (commonly used for APIs with JWT or OAuth2)
☑️ OAuth2 Integration (for secure, delegated access)
1. Basic Authentication
Basic Authentication requires a username and password sent in the Authorization
header as a Base64-encoded string (username:password
).
Example: Manually Adding Basic Auth Header
import java.util.Base64;
public Post getPostWithBasicAuth(int id) {
String username = "admin";
String password = "password";
String authHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
return restClient.get()
.uri("/posts/{id}", id)
.header("Authorization", authHeader) // Adds the Basic Auth header
.retrieve()
.body(Post.class);
}
☑️ Manually encodes username & password as Base64 for authentication.
Better Approach: Using defaultHeader
for Global Authentication
Instead of setting headers in every request, configure them globally:
import org.springframework.stereotype.Component;
import org.springframework.boot.web.client.RestClientCustomizer;
import java.util.Base64;
@Component
public class RestClientBasicAuthCustomizer implements RestClientCustomizer {
@Override
public void customize(RestClient.Builder builder) {
String username = "admin";
String password = "password";
String authHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
builder.defaultHeader("Authorization", authHeader); // Applies Basic Auth to all requests
}
}
☑️ Now, all requests include Basic Authentication automatically.
2. Bearer Token Authentication
Many APIs use Bearer Tokens (e.g., JWTs or API tokens) for authentication. The token is sent in the Authorization
header.
Example: Manually Adding a Bearer Token
public Post getPostWithBearerToken(int id) {
String token = "your-jwt-token";
return restClient.get()
.uri("/posts/{id}", id)
.header("Authorization", "Bearer " + token) // Adds Bearer token
.retrieve()
.body(Post.class);
}
☑️ Manually sets the token in each request.
Better Approach: Global Bearer Token Configuration
Instead of setting the token manually, apply it globally to all requests:
import org.springframework.stereotype.Component;
import org.springframework.boot.web.client.RestClientCustomizer;
@Component
public class RestClientBearerTokenCustomizer implements RestClientCustomizer {
@Override
public void customize(RestClient.Builder builder) {
String token = "your-jwt-token"; // Retrieve from a config or auth service
builder.defaultHeader("Authorization", "Bearer " + token); // Applies token globally
}
}
☑️ Now all RestClient
requests include the Bearer Token.
3. OAuth2 Integration with RestClient
OAuth2 authentication allows secure access to APIs without exposing credentials. Spring Boot provides built-in OAuth2 support via spring-security-oauth2-client
.
Step 1: Add Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
☑️ This enables Spring Boot’s OAuth2 client support.
Step 2: Configure OAuth2 in application.yml
spring:
security:
oauth2:
client:
registration:
my-oauth-client:
client-id: your-client-id
client-secret: your-client-secret
authorization-grant-type: client_credentials
provider: my-oauth-provider
provider:
my-oauth-provider:
token-uri: https://auth-server.com/oauth/token
☑️ Defines OAuth2 client details and token URL.
Step 3: Inject OAuth2AuthorizedClientService
to Get Tokens
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.stereotype.Service;
@Service
public class OAuth2TokenService {
private final OAuth2AuthorizedClientService authorizedClientService;
public OAuth2TokenService(OAuth2AuthorizedClientService authorizedClientService) {
this.authorizedClientService = authorizedClientService;
}
public String getAccessToken(OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(), authentication.getName());
return client.getAccessToken().getTokenValue();
}
}
☑️ Retrieves an OAuth2 token dynamically.
Step 4: Use OAuth2 Token in RestClient
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
@Service
public class ApiService {
private final RestClient restClient;
private final OAuth2TokenService tokenService;
public ApiService(RestClient restClient, OAuth2TokenService tokenService) {
this.restClient = restClient;
this.tokenService = tokenService;
}
public Post getPostWithOAuth2(int id, OAuth2AuthenticationToken authentication) {
String token = tokenService.getAccessToken(authentication);
return restClient.get()
.uri("/posts/{id}", id)
.header("Authorization", "Bearer " + token)
.retrieve()
.body(Post.class);
}
}
☑️ Dynamically fetches and applies OAuth2 tokens for API calls.
7. Advanced Usage of RestClient
Beyond basic HTTP requests, RestClient
supports advanced use cases such as:
☑️ Streaming Large Responses (handling large payloads efficiently)
☑️ Multipart/Form-Data Requests (for file uploads and complex forms)
1. Streaming Large Responses
APIs may return large JSON/XML files, causing memory issues if loaded entirely at once. Streaming allows processing data incrementally.
🔹 Using RestClient with InputStream
import java.io.InputStream;
import java.util.Scanner;
public void streamLargeResponse() {
try (InputStream responseStream = restClient.get()
.uri("/large-data")
.retrieve()
.body(InputStream.class);
Scanner scanner = new Scanner(responseStream)) {
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine()); // Process data line by line
}
}
}
☑️ Processes large responses without loading everything into memory.
🔹 Using WebClient for Non-Blocking Streaming
For truly non-blocking streaming, use WebClient
:
import reactor.core.publisher.Flux;
public void streamLargeResponseAsync() {
Flux<String> responseFlux = webClient.get()
.uri("/large-data")
.retrieve()
.bodyToFlux(String.class);
responseFlux.subscribe(System.out::println); // Processes data asynchronously
}
☑️ Ideal for large datasets that need real-time processing.
2. Working with Multipart/Form-Data Requests
Some APIs require multipart requests to upload files or forms. RestClient
supports this with MultipartBodyBuilder
.
🔹 Example: Uploading a File with RestClient
import org.springframework.http.MediaType;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
public void uploadFile() {
FileSystemResource file = new FileSystemResource("path/to/file.jpg");
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file);
String response = restClient.post()
.uri("/upload")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body(String.class);
System.out.println("Upload response: " + response);
}
☑️ Sends a multipart request with a file.
🔹 Example: Uploading Multiple Files and Form Data
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.MultiValueMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.http.MediaType;
public void uploadMultipleFiles() {
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file1", new FileSystemResource("file1.jpg"));
body.add("file2", new FileSystemResource("file2.png"));
body.add("description", "Uploading multiple files");
String response = restClient.post()
.uri("/upload-multiple")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.retrieve()
.body(String.class);
System.out.println("Upload response: " + response);
}
☑️ Uploads multiple files along with form data.
8. Testing with RestClient
Testing API interactions is essential to ensure reliability, correctness, and error handling in applications that use RestClient
. This section covers:
☑️ Mocking HTTP calls using MockRestServiceServer
☑️ Unit Testing (isolating API calls)
☑️ Integration Testing (testing API calls in a real Spring Boot environment)
1. Mocking HTTP Calls Using MockRestServiceServer
When writing unit tests, you don’t want to make real API calls. Instead, you can mock API responses using MockRestServiceServer
.
🔹 Step 1: Dependency
Ensure you have spring-boot-starter-test
in your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
☑️ Includes JUnit, Mockito, and Spring Test utilities for mocking API calls.
🔹 Step 2: Create a RestClient
Service for Testing
import org.springframework.web.client.RestClient;
import org.springframework.stereotype.Service;
@Service
public class PostService {
private final RestClient restClient;
public PostService(RestClient.Builder builder) {
this.restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
}
public Post getPostById(int id) {
return restClient.get()
.uri("/posts/{id}", id)
.retrieve()
.body(Post.class);
}
}
☑️ This service makes HTTP requests using RestClient
.
🔹 Step 3: Mock API Responses in Unit Tests
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
import org.springframework.http.client.ClientHttpRequestFactory;
public class PostServiceTest {
private MockRestServiceServer mockServer;
private PostService postService;
@BeforeEach
void setUp() {
RestClient restClient = RestClient.builder().baseUrl("https://jsonplaceholder.typicode.com").build();
mockServer = MockRestServiceServer.createServer((ClientHttpRequestFactory) restClient);
postService = new PostService(restClient);
}
@Test
void testGetPostById() {
String mockResponse = "{\"id\": 1, \"title\": \"Mock Post\", \"body\": \"This is a test\"}";
mockServer.expect(requestTo("https://jsonplaceholder.typicode.com/posts/1"))
.andRespond(withSuccess(mockResponse, MediaType.APPLICATION_JSON));
Post post = postService.getPostById(1);
assertNotNull(post);
assertEquals(1, post.getId());
assertEquals("Mock Post", post.getTitle());
}
}
☑️ This test mocks an API response and verifies the returned Post
object.
2. Writing Integration Tests
Integration tests validate the end-to-end behavior of your application by making real API calls.
🔹 Step 1: Enable Spring Boot Testing
Use @SpringBootTest
to run tests in a real Spring Boot environment.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testGetPostById() {
ResponseEntity<Post> response = restTemplate.getForEntity("/api/posts/1", Post.class);
assertNotNull(response.getBody());
assertEquals(1, response.getBody().getId());
}
}
☑️ Runs the entire Spring Boot application and tests the real API.
3. Using WireMock for Advanced Mocking
WireMock is useful when you need more control over mock API responses, such as:
- Simulating network delays
- Returning different status codes
- Mocking authentication flows
🔹 Step 1: Add WireMock Dependency
<dependency>
<groupId>org.wiremock.integrations</groupId>
<artifactId>wiremock-spring-boot</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
☑️ This integrates WireMock into Spring Boot.
🔹 Step 2: Configure WireMock for Testing
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.jupiter.api.Assertions.*;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.*;
public class WireMockTest {
private static WireMockServer wireMockServer;
@BeforeAll
static void startWireMock() {
wireMockServer = new WireMockServer(8089);
wireMockServer.start();
}
@Test
void testWireMockAPI() {
wireMockServer.stubFor(get(urlEqualTo("/posts/1"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"id\":1, \"title\":\"Mocked Post\"}")));
String response = restClient.get()
.uri("http://localhost:8089/posts/1")
.retrieve()
.body(String.class);
assertTrue(response.contains("Mocked Post"));
}
@AfterAll
static void stopWireMock() {
wireMockServer.stop();
}
}
☑️ WireMock allows powerful API simulations with request matching and response control.