Calling HTTP APIs with RestClient

Spring Boot · Practical · 12 min

Microservices and integrations constantly call external HTTP APIs.

Spring Framework 6.1 introduced RestClient, a fluent, synchronous HTTP client that replaces RestTemplate for new code (RestTemplate still works but is in maintenance mode). Spring Boot auto-configures a RestClient.Builder bean with the application's JSON message converters; inject the builder, set a base URL and defaults, and build a client:

RestClient client = builder.baseUrl("https://api.example.com").build();

A call reads like the HTTP request: client.get().uri("/users/{id}", id).retrieve().body(User.class). Use toEntity to get status and headers too, body(new ParameterizedTypeReference<List<User>>() {}) for generic types, and .post().body(dto) to send JSON.

By default a 4xx or 5xx response throws RestClientResponseException (HttpClientErrorException/HttpServerErrorException). Customise with onStatus(HttpStatusCode::is4xxClientError, (req, res) -> ...).

HTTP interfaces are a declarative alternative: annotate a Java interface with @GetExchange("/users/{id}") methods, then create an implementation with HttpServiceProxyFactory backed by a RestClient. The interface reads like a contract and is easy to mock in tests.

Always configure timeouts (connect and read) through the request factory, for example ClientHttpRequestFactorySettings or the spring.http.client.* properties in recent Boot versions; an unbounded call to a slow service ties up threads. For reactive apps use WebClient instead.

Example

record User(long id, String name) {}

@Service
class UserClient {
    private final RestClient client;

    UserClient(RestClient.Builder builder) {          // auto-configured by Boot
        this.client = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
    }

    User find(long id) {
        return client.get().uri("/users/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
                throw new IllegalArgumentException("no user " + id);
            })
            .body(User.class);
    }

    List<User> all() {
        return client.get().uri("/users").retrieve()
            .body(new ParameterizedTypeReference<List<User>>() {});
    }
}
// find(1) -> User[id=1, name=Leanne Graham]

Remember

  • RestClient is the modern synchronous client; RestTemplate is in maintenance
  • Inject Boot's RestClient.Builder to inherit JSON configuration
  • 4xx/5xx throw by default; customise with onStatus
  • Always set connect and read timeouts

Watch out for

  • Creating RestClient.create() everywhere, losing Boot's configured converters, observability and timeouts.

Check yourself

  1. What happens by default when retrieve().body(User.class) receives a 404?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-boot/reference/io/rest-client.html