RestClient in Spring Boot: Simple Guide to Synchronous Microservice Communication

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

Leave a Reply