RestClient in Spring Boot: Simple Guide to Synchronous Microservice Communication
If you are learning Spring Boot Microservices, you will often see one service calling another service.
For example:
Order Service ---> Product Service
The Order Service needs some information from the Product Service.
But how can one microservice call another?
One modern option is RestClient.
In this article, we will learn RestClient from the very beginning in a very simple way.
What Will We Learn?
In this article, we will understand:
- What is RestClient?
- Why was RestClient introduced?
- RestTemplate vs RestClient
- What is a Fluent API?
- How to make GET requests
- How to make POST requests
- How to make DELETE requests
- How to handle exceptions
- How
exchange()works - How to add interceptors
- How RestClient works in a microservice example
1. First Understand the Problem
Imagine we have two microservices:
OrderService
Port: 8081
ProductService
Port: 8082
The Order Service wants to get product information.
So it sends a request:
OrderService
|
| GET /products/10
↓
ProductService
|
| Product information
↓
OrderService
This is called synchronous communication.
Why?
Because OrderService waits for ProductService to respond.
In simple words:
“I asked you a question. I will wait here until you answer.”
The uploaded material describes RestClient as synchronous/blocking communication where the client waits for the server response.
2. What Is RestClient?
RestClient is a Spring API used to make HTTP calls to REST APIs.
For example:
String response = restClient
.get()
.uri("http://localhost:8082/products/1")
.retrieve()
.body(String.class);
That’s it!
We are basically saying:
GET
↓
Go to this URL
↓
Get the response
↓
Give me the response as String
RestClient was introduced with Spring Framework 6.0+ and Spring Boot 3.0+ according to the provided material. It is designed as a modern, fluent API.
3. Why Do We Need RestClient?
Before RestClient, many Spring applications used RestTemplate.
RestTemplate is still important to understand, especially when working on existing applications.
However, the provided material highlights some limitations:
- It has many overloaded methods.
- The API can become difficult to remember and maintain.
- It was designed before concepts such as retry and circuit breaker became common requirements.
- It is now in maintenance mode.
So Spring introduced:
RestTemplate
↓
RestClient
RestClient provides a more modern and fluent way of making HTTP requests.
4. RestTemplate vs RestClient
Let’s keep it very simple.
RestTemplate
restTemplate.getForObject(
url,
String.class
);
RestClient
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
The second approach reads almost like English.
You can think of it as:
GET
→ this URI
→ retrieve the response
→ give me the body
→ convert it to String
That’s the main idea behind the Fluent API.
5. What Is a Fluent API?
Don’t be scared by the name.
Fluent API simply means chaining method calls.
For example:
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
We are calling one method after another:
get()
↓
uri()
↓
retrieve()
↓
body()
Each step gives us an object that provides the methods for the next step.
The source specifically explains Fluent API as chaining method calls, where each method returns an object exposing the operations available for the next step.
6. Create a RestClient Bean
We can create a RestClient bean in our Spring Boot application.
@Configuration
public class AppConfig {
@Bean
public RestClient restClientInstance() {
return RestClient.create();
}
}
Now Spring knows:
“Whenever someone asks for a RestClient, give them this object.”
The provided example uses exactly this basic configuration.
7. Create ProductService
Let’s create a simple Product Service.
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public String getProduct(
@PathVariable String id) {
return "Product fetched with id: " + id;
}
}
Suppose ProductService is running on:
http://localhost:8082
Now we can call:
GET http://localhost:8082/products/10
Response:
Product fetched with id: 10
8. Call ProductService from OrderService
Now comes the interesting part.
Our OrderService is running on:
http://localhost:8081
ProductService is running on:
http://localhost:8082
We want:
OrderService
|
| RestClient
↓
ProductService
Our controller can look like this:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
RestClient restClient;
@GetMapping("/{id}")
public ResponseEntity<String> getOrder(
@PathVariable String id) {
String response = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.retrieve()
.body(String.class);
System.out.println(
"Response from Product API: "
+ response
);
return ResponseEntity.ok(
"Order call successful"
);
}
}
The uploaded example demonstrates this same OrderService → ProductService call using get(), uri(), retrieve() and body().
9. Let’s Understand This Code Like a Beginner
Look at this:
String response = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.retrieve()
.body(String.class);
Don’t try to memorize everything.
Read it step by step.
Step 1: get()
.get()
Means:
“I want to send a GET request.”
Step 2: uri()
.uri("http://localhost:8082/products/" + id)
Means:
“Send the request to this URL.”
Step 3: retrieve()
.retrieve()
Means:
“Go and get the response.”
Step 4: body()
.body(String.class)
Means:
“Give me the response body as a String.”
That’s all!
So remember:
GET
↓
URI
↓
Retrieve
↓
Body
10. GET Request With Headers
We can also send headers.
For example:
String response = restClient
.get()
.uri("http://localhost:8082/products/" + id)
.accept(MediaType.APPLICATION_JSON)
.header("X-Custom-Header", "xyz")
.retrieve()
.body(String.class);
Here:
.accept(MediaType.APPLICATION_JSON)
means:
“I want JSON as the response format.”
And:
.header("X-Custom-Header", "xyz")
adds a custom HTTP header.
The source demonstrates this GET flow with both accept() and a custom header.
11. POST Request
Now suppose we want to create a product.
For POST, the flow becomes:
POST
↓
URI
↓
Headers
↓
Body
↓
Retrieve response
Example:
ResponseEntity<ProductEntity> response =
restClient
.post()
.uri("http://localhost:8082/products/create")
.accept(MediaType.APPLICATION_JSON)
.header(
"Content-Type",
"application/json"
)
.body(
new ProductEntity("Ice-cream")
)
.retrieve()
.toEntity(ProductEntity.class);
Then we can get the response body:
ProductEntity responseBody =
response.getBody();
The provided example uses this same POST flow and maps the response into ProductEntity.
12. Why Do We Use body()?
Suppose we want to send this product:
ProductEntity product =
new ProductEntity("Ice-cream");
We need to send this object to the server.
That’s why we use:
.body(product)
In simple words:
“Hey ProductService, here is the data I want to send.”
13. body() vs toEntity()
This is easy to remember.
If you only want the body:
.body(Product.class)
You get the object.
If you want the complete response:
.toEntity(Product.class)
You get:
HTTP Status
Headers
Body
For example:
ResponseEntity<Product> response =
restClient
.get()
.uri(url)
.retrieve()
.toEntity(Product.class);
Then:
response.getBody();
response.getStatusCode();
14. DELETE Request
Deleting is also very simple.
ResponseEntity<Void> response =
restClient
.delete()
.uri(
"http://localhost:8082/products/" + id
)
.retrieve()
.toBodilessEntity();
HttpStatusCode status =
response.getStatusCode();
Read it like English:
DELETE
↓
Go to this URI
↓
Retrieve response
↓
There is no body
↓
Give me the status
The provided material demonstrates this exact pattern with toBodilessEntity().
15. Exception Handling
Now let’s talk about an important topic.
What happens if ProductService returns:
400 Bad Request
or:
500 Internal Server Error
We may want to handle these errors ourselves.
RestClient provides ways to customize status handling.
For example:
String responseObj = restClient
.get()
.uri(
"http://localhost:8082/products/" + id
)
.retrieve()
.onStatus(response -> {
if (response.getStatusCode()
.is4xxClientError()) {
throw new MyCustomException(
"Invalid request passed"
);
}
if (response.getStatusCode()
.is5xxServerError()) {
throw new RuntimeException(
"Something wrong at server"
);
}
return false;
})
.body(String.class);
The source demonstrates onStatus() for handling 4xx and 5xx responses.
16. What Does onStatus() Mean?
Think of it like a security guard.
The request comes back.
The security guard checks:
Is it 4xx?
|
YES → Throw client error
Is it 5xx?
|
YES → Throw server error
Otherwise
|
Continue
So:
.onStatus(...)
allows us to decide what should happen for particular HTTP status codes.
17. What If We Want Full Control?
Sometimes we don’t want RestClient to automatically build the response.
We want to control everything ourselves.
For this situation, we can use:
exchange()
The source specifically introduces exchange() when full control over response building and exception handling is required.
Example:
String responseObj = restClient
.get()
.uri(
"http://localhost:8082/products/" + id
)
.exchange((request, response) -> {
if (response.getStatusCode()
.is4xxClientError()) {
throw new MyCustomException(
"Invalid request passed"
);
}
if (response.getStatusCode()
.is5xxServerError()) {
throw new RuntimeException(
"Something wrong at server"
);
}
return StreamUtils.copyToString(
response.getBody(),
StandardCharsets.UTF_8
);
});
18. retrieve() vs exchange()
This is an important interview question.
retrieve()
Use it when you want RestClient to handle the normal response processing.
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
exchange()
Use it when you want more control.
restClient
.get()
.uri(url)
.exchange((request, response) -> {
// your own logic
});
In simple words:
retrieve()
↓
"Spring, you handle the response."
exchange()
↓
"Spring, give me the response.
I will handle it myself."
The source’s examples show this distinction through onStatus() with retrieve() and direct response handling with exchange().
19. What Is an Interceptor?
Now let’s learn another useful feature.
Imagine every request from OrderService must contain:
X-Custom-Header: myvalue
Instead of writing this every time:
.header("X-Custom-Header", "myvalue")
we can use an Interceptor.
An interceptor can look at a request before it is sent and modify it.
Think of it like this:
OrderService
|
↓
Interceptor
|
| Add Header
↓
ProductService
20. Creating an Interceptor
We can create:
public class MyCustomRequestInterceptor
implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution)
throws IOException {
request.getHeaders().add(
"X-Custom-Header",
"myvalue"
);
return execution.execute(request, body);
}
}
The interceptor adds:
X-Custom-Header: myvalue
to the outgoing request.
21. Register the Interceptor
Now we need to tell our RestClient:
“Please use my interceptor.”
@Configuration
public class AppConfig {
@Bean
public RestClient restClientInstance(
ClientHttpRequestInterceptor
myCustomInterceptor) {
return RestClient.builder()
.requestInterceptor(
myCustomInterceptor
)
.build();
}
@Bean
public ClientHttpRequestInterceptor
customRequestInterceptor() {
return new MyCustomRequestInterceptor();
}
}
Now the interceptor becomes part of the RestClient configuration.
22. What Happens When We Make a Request?
Suppose we write:
restClient
.get()
.uri(
"http://localhost:8082/products/" + id
)
.retrieve()
.toEntity(String.class);
Behind the scenes:
OrderService
|
↓
RestClient
|
↓
Interceptor
|
| Adds X-Custom-Header
↓
ProductService
The ProductService can then see:
X-Custom-Header: myvalue
The uploaded example shows the header being visible in ProductService after the interceptor runs.
23. Why Are Interceptors Useful?
Imagine you have 100 API calls.
You want to add the same header to every request.
Without an interceptor:
.header("X-Custom-Header", "xyz")
would have to be repeated many times.
With an interceptor:
One place
↓
Interceptor
↓
Many requests automatically get the header
This can make common request processing easier to centralize.
24. Complete RestClient Flow
Let’s put everything together.
Suppose:
OrderService
Port: 8081
ProductService
Port: 8082
OrderService wants ProductService data.
The code:
String response = restClient
.get()
.uri(
"http://localhost:8082/products/" + id
)
.retrieve()
.body(String.class);
The flow is:
GET
OrderService ----------------> ProductService
| |
| |
| Process request
| |
|<-----------------------------|
Response
And because this is synchronous:
OrderService
|
| "I will wait for your response."
↓
ProductService
|
| "Here is the response."
↓
OrderService continues
25. Easy Way to Remember RestClient
If you forget everything else, remember this:
GET
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
Think:
GET → URI → RETRIEVE → BODY
POST
restClient
.post()
.uri(url)
.body(object)
.retrieve()
.toEntity(Product.class);
Think:
POST → URI → BODY → RETRIEVE → RESPONSE
DELETE
restClient
.delete()
.uri(url)
.retrieve()
.toBodilessEntity();
Think:
DELETE → URI → RETRIEVE → NO BODY
Advanced
.exchange(...)
Think:
“I want to control the response myself.”
26. RestClient vs RestTemplate — Simple Explanation
Imagine two ways of ordering food.
RestTemplate
You have many different forms:
Form A
Form B
Form C
Form D
Form E
...
You need to remember which form to use.
RestClient
You tell the waiter:
GET
↓
This restaurant
↓
Bring me the food
↓
Give it to me as Product
The API is designed around a fluent sequence of operations.
The source presents RestClient as the alternative to RestTemplate with a modern, fluent API intended to be more readable and maintainable.
27. Quick Interview Questions
What is RestClient?
RestClient is a synchronous HTTP client API provided by Spring for making REST API calls.
When was RestClient introduced?
The provided material states Spring Framework 6.0+ and Spring Boot 3.0+.
Is RestClient synchronous?
Yes.
What is Fluent API?
It means chaining method calls.
Example:
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
What does retrieve() do?
It starts the response processing flow after configuring the request.
What does body() do?
It extracts the response body and converts it to the requested type.
What does toEntity() do?
It returns the response as a ResponseEntity, allowing access to response information such as status and body.
What is exchange()?
It gives more direct control over the request/response handling.
What is an interceptor?
An interceptor can modify or process an HTTP request before it is sent.
Conclusion
If you are working with Spring Boot 3+, RestClient is an important API to understand for synchronous HTTP communication.
The basic pattern is extremely simple:
restClient
.get()
.uri(url)
.retrieve()
.body(String.class);
Just remember the story:
I want to GET something
↓
Tell me the URI
↓
Get the response
↓
Give me the body
For POST:
POST
↓
URI
↓
BODY
↓
RESPONSE
For DELETE:
DELETE
↓
URI
↓
RESPONSE
And when you need complete control:
exchange()
Finally, if you need something to happen automatically for many requests, such as adding a common header, an interceptor can be configured with RestClient.
SEO Title
RestClient in Spring Boot: Simple Guide with GET, POST, DELETE, Exchange & Interceptor
Meta Description
Learn Spring Boot RestClient with simple examples. Understand GET, POST, DELETE, exception handling, exchange(), Fluent API and interceptors step by step.
URL Slug
/restclient-spring-boot/
Tags
Java · Spring Boot · RestClient · Microservices · REST API · Spring Framework · Java Microservices · RestTemplate