Skip to main content

Http Requests With Rest Client in Spring Framework 6

Spring Restclient HTTP Rest Openai
Author
Harpal Singh
Software Engineer
Table of Contents

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.

Related

Understanding Spring's @Scheduled Cron Syntax
Spring Cron Scheduling
Testcontainers in Spring Boot Integration Tests
Spring Testcontainers Testing
Get Values Defined in Properties File in Spring
Spring Properties Basics