1. Introduction
In this tutorial, we’ll learn how to make HTTP requests using the RestClient
HTTP client introduced in Spring 6.1 (M2).
First, we’ll understand the RestClient
API. Then, we’ll see how to make GET, POST, PUT, and DELETE requests using the RestClient
API. Finally, we’ll look at a practical example of making HTTP requests to the OpenAI API and call ChatGPT to generate some text for us.
2. Understanding RestClient
A while ago, the Spring framework introduced the WebClient
to make non-blocking HTTP requests. WeClient
uses a reactive paradigm and is suitable in applications that work well in asynchronous mode and streaming scenarios. Therefore, using it in a non-reactive application adds unnecessary complexity and requires us to understand the Mono
and Flux
types very well. Additionally, WebClient
don’t make the program any more performant, as we need to wait for the response anyway.
So, the Spring framework introduced the RestClient
API to make synchronous HTTP requests. A more modern approach to the classic RestTemplate
. That doesn’t mean that RestTemplate
is deprecated. In fact, under the hood, they both use the same infrastructure, such as message converters, request factories, etc. However, it is a good idea to stick with the RestClient
API as it will be the focus of future iterations of the spring framework and will get more features.
RestClient
uses the HTTP libraries such as the JDK HttpClient, Apache HttpComponents, and others. It makes use of the library that is available in the classpath. So, RestClient abstracts away all the http client details and uses the most appropriate one, depending on the dependencies we have in our project.
3. Create RestClient
We create RestClient
using the create()
method, which returns a new instance with default settings:
RestClient simpleRestClient = RestClient.create();
If we want to customize the RestClient
, we can use the builder()
method and use the fluent API to set the configurations we need:
RestClient customRestClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MappingJackson2HttpMessageConverter()))
.baseUrl("https://example.com")
.defaultHeader("Authorization", "Bearer your-token")
.defaultHeader("Content-Type", "application/json")
.build();
Our custom RestClient
uses the Apache client to make HTTP requests. Then, we add a custom message converter to handle JSON serialization and deserialization using Jackson. We set the base URL, default headers, and content type for all requests made with this client.
Keep in mind most of these configurations are already set up by default. Now, we have a customized RestClient
that we can use to make HTTP requests. We can reuse this RestClient
instance across our application without issues as it is thread-safe.
We can also migrate to RestClient
from RestTemplate
incrementally and reuse the existing configurations to create a RestClient
:
RestTemplate restTemplate = new RestTemplate();
RestClient restClientFromRestTemplate = RestClient.create(restTemplate);
4. Making HTTP Requests
In this section, we’ll briefly explain how to make GET, POST, PUT, and DELETE requests using the RestClient
API. We’ll use a fake API called awesomeapi.com to demonstrate the requests. For simplicity, we’ll use the retrieve()
method to make the requests and get the response as a String
.
4.1. Making a GET Request
The most common type of HTTP request is the GET request, which we use to get data from a server. The RestClient
provides a straightforward way to make GET requests with various options for handling parameters and response types:
String response = simpleRestClient.get()
.uri("https://awesomeapi.com/string/")
.retrieve()
.body(String.class);
record Item(String id, String name, double price) {
} // JSON: {"id": 123, "name": "Product", "price": 99.99}
Item item = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", 123)
.retrieve()
.body(Item.class);
In this snippet, we are making two GET requests. The first just gets a raw String
response. The second receives a JSON response of an Item
object with name
and price
fields. The body()
method automatically deserializes the response into a valid instance of Item
using Jackson under the hood. So, if the response differs from the Item
structure, it throws an exception.
4.2. Making a POST Request
To send data to the server, we use the POST request. More formally, POST requests create new resources on the server. As we saw with GET requests, the RestClient
makes it easy to send JSON-encoded objects as the request body:
record ItemRequest(String name, double price) {
} // JSON: {"name": "Product", "price": 99.99}
ItemRequest newItem = new ItemRequest("Product", 99.99);
Item createdItem = simpleRestClient.post()
.uri("https://awesomeapi.com/item/")
.contentType(MediaType.APPLICATION_JSON)
.body(newItem)
.retrieve()
.body(Item.class);
In this example, we create a new ItemRequest
object and send it to the server. The contentType()
method specifies that we’re sending JSON data, and the body()
method automatically serializes the Item
object into a valid JSON. Usually, the server responds with the resource it created, which we can deserialize into an Item
object. The difference between Item
and ItemRequest
is that Item
has an id
field generated by the server.
4.3. Making a PUT Request
Let’s say we want to update an existing resource on the server. We can use the PUT request for this purpose. Similar to POST requests, PUT requests include a request body of a resource we want to update:
ItemRequest updatedItem = new ItemRequest("Updated Product", 129.99);
Item updated = simpleRestClient.put()
.uri("https://awesomeapi.com/item/{id}", 123)
.contentType(MediaType.APPLICATION_JSON)
.body(updatedItem)
.retrieve()
.body(Item.class);
The PUT request example shows how to update an existing product. We specify the id of the product in the uri()
method and provide the updated product data in the request body. The response contains the updated product information as confirmed by the server.
Notice that the uri()
method uses path variables syntax, a convenient way to specify dynamic parts of the URI. Some APIs use query parameters instead of path variables. To add query parameters, we can use the UriBuilder
and set the path, the parameters using the queryParam()
method, and finally build the Uri
to pass to the uri()
method:
Item updatedWithQuery = simpleRestClient.put()
.uri(uriBuilder ->
uriBuilder.path("https://awesomeapi.com/item/")
.queryParam("id", 123)
.build())
.contentType(MediaType.APPLICATION_JSON)
.body(updatedItem)
.retrieve()
.body(Item.class);
Query parameters are added to the URI after the ?
character. So the resulting uri will be https://awesomeapi.com/item/?id=123
.
4.4. Making a DELETE Request
Finally, let’s look at how to delete a resource from the server using the DELETE request:
simpleRestClient.delete()
.uri("https://awesomeapi.com/item/{id}", 123)
.retrieve()
.toBodilessEntity();
In the DELETE example, we use toBodilessEntity()
since DELETE requests typically don’t return a response body. The path variable syntax specifies which resource to delete.
If the deletion is successful, no exception is thrown. This is valid for all requests; if the server responds with an error status code, an exception will be thrown. We can use try-catch blocks to handle these exceptions, but there are more sane ways to handle these errors. We’ll see that in the next section.
5. Handling Errors using onStatus()
When working with REST APIs, we need to handle errors gracefully. We can’t use try-catch blocks for every request, as it results in a boilerplate and unreadable code. The RestClient
provides a very clean way to handle errors based on the response’s status code. We use the onStatus()
method to handle different status codes and provide a more readable way to handle failures:
String id = "123";
item = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", id)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new RuntimeException("Item not found with id: " + id);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new RuntimeException("Service is currently unavailable");
})
.body(Item.class);
In this example, we make a GET request to fetch an Item
object with a specific id. If the server responds with a 4xx client error, we throw an exception with a message saying the item was not found on the server. Similarly, if we get a 5xx server error, we throw another RuntimeException
indicating that the service is unavailable. This shows how to differentiate between different types of errors; in real-world scenarios, we have to implement the error handling logic based on the requirements. For example, we might have temporary errors that require retry logic.
6. Understanding retrieve()
vs exchange()
The retrieve()
simplifies HTTP requests while covering most use cases. However, sometimes, we need more control over the HTTP request and response. For example, we might need to read the response headers that don’t fit into the onStatus()
method. We can use the exchange()
method in such cases. Be aware using exchange()
won’t use status handlers, as it already returns the full response, allowing us to perform any error handling we need.
For example, we can implement the same 404 error handling done in the previous section using exchange()
:
Item itemResponse = simpleRestClient.get()
.uri("https://awesomeapi.com/item/{id}", id)
.exchange((request, response) -> {
HttpHeaders headers = response.getHeaders();
String etag = headers.getETag();
if (response.getStatusCode().is4xxClientError()) {
throw new RuntimeException("Item not found with id: " + id);
} else if(response.getStatusCode().is5xxServerError()){
throw new RuntimeException("Service is currently unavailable");
}
log.info("Got request with ETAG: " + etag);
return response.bodyTo(Item.class);
});
We manually created the error handling logic using if-else statements in this example. Using handlers reduces the code’s readability but allows us to access headers from the response, like the ETag
, which is otherwise impossible with the retrieve()
method.
Let’s try a more practical example of calling a real API.
7. Making HTTP Calls to OpenAI API
Rest API is a great way to interact with a third-party service. In this section, we’ll use the OpenAI API to generate some text using the ChatGPT model. We’ll use the RestClient
to make a POST request to the OpenAI API and get the response.
First, we need to get an API key from the OpenAI website. It is a good practice not to include the API key in the codebase and to use environment variables or a secure way to store it. For this example, we’ll assume we have an environment variable named OPENAI_API_KEY
that contains the API key. To read it, we just use the getenv()
method from the System
class.
String apiKey = System.getenv("OPENAI_API_KEY");
Next, we create some helper record
classes to represent the request and response structures required by the OpenAI API:
record ChatRequest(String model, List<Message> messages) {}
record Message(String role, String content) {}
record ChatResponse(List<Choice> choices) {}
record Choice(Message message) {}
Now, we create a RestClient
instance with the necessary headers for authentication and content type:
RestClient openAiClient = RestClient.builder()
.baseUrl("https://api.openai.com/v1")
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
Finally, we make a POST request to the OpenAI API to generate some text using the ChatGPT model:
ChatRequest request = new ChatRequest(
"gpt-4o-mini",
List.of(new Message("user", "What is the brightest star in our galaxy?"))
);
ChatResponse response = openAiClient.post()
.uri("/chat/completions")
.body(request)
.retrieve()
.body(ChatResponse.class);
log.info("ChatGPT: " + response.choices().get(0).message().content());
If everything goes well, we will see the response from ChatGPT:
INFO: ChatGPT: The brightest star in our galaxy, the Milky Way, is typically considered to be **Sagittarius A***. However, strictly speaking, Sagittarius A* is actually the supermassive black hole at the center of the Milky Way. The title of the brightest visible star from Earth is generally given to **Sirius**, which is part of the constellation Canis Major. Sirius is approximately 8.6 light-years away and is about twice as massive as the Sun. It is often referred to as the "Dog Star."
This example shows how to use the RestClient
to interact with a real-world API like the one provided by OpenAI to handle complex JSON structures and authentication requirements clean and type-safely.
8. Conclusion
In this article, we learned about the new RestClient
introduced in Spring Framework 6.1’s new RestClient
API for making HTTP requests. We learned how to create and configure a RestClient
, make HTTP requests (GET, POST, PUT, DELETE), handle errors, and work with real-world APIs like OpenAI.
We can look for the complete code on Github.