Spring Boot REST Client

Most real applications talk to other systems. A shop app asks a payment service to charge a card, a travel app asks a weather service for a forecast, and a dashboard asks an internal service for user details. A REST client is the code that sends these requests and reads the replies.

Spring Boot offers three tools for the job: RestTemplate, WebClient, and RestClient. Each tool fits a different situation, and picking the right one keeps the code clean.

The Supplier Phone Call Idea

A bakery owner needs flour and phones a supplier. The owner states the order, waits for the answer, and writes down the delivery date. A REST client behaves like that phone call. The application dials another service, states a request, and processes the answer.

 Your Spring Boot app                     Other service
 +------------------+   GET /weather/Paris   +------------------+
 |   REST client    | ---------------------> |   Weather API    |
 |                  | <--------------------- |                  |
 +------------------+   200 {"temp": 18}     +------------------+

Choosing a Tool

ToolStyleBest For
RestClientBlocking, fluent API (Spring Boot 3.2 and later)New projects with standard web starter
WebClientNon-blocking, reactiveHigh-concurrency apps and streaming data
RestTemplateBlocking, older template styleExisting projects that already use it

RestTemplate stays in maintenance mode, so new code benefits from RestClient or WebClient.

Setting Up RestClient

The web starter already contains RestClient. Create one bean with a base address, and inject it wherever needed.

@Configuration
public class ClientConfig {

    @Bean
    public RestClient weatherClient() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3000);
        factory.setReadTimeout(5000);

        return RestClient.builder()
                .baseUrl("https://api.example.com")
                .requestFactory(factory)
                .defaultHeader("Accept", "application/json")
                .build();
    }
}

Timeouts protect the application. Without them, a slow service can hold threads for minutes and freeze your own system.

Sending a GET Request

@Service
public class WeatherService {

    private final RestClient client;

    public WeatherService(RestClient weatherClient) {
        this.client = weatherClient;
    }

    public Weather findByCity(String city) {
        return client.get()
                .uri("/weather/{city}", city)
                .retrieve()
                .body(Weather.class);
    }
}

The uri method fills the curly-brace placeholder. The body method converts the JSON reply into a Java object through Jackson.

Sending a POST Request

public PaymentResult charge(PaymentRequest request) {
    return client.post()
            .uri("/payments")
            .contentType(MediaType.APPLICATION_JSON)
            .body(request)
            .retrieve()
            .body(PaymentResult.class);
}

Handling Errors

Remote services fail in many ways: wrong input, missing data, or an internal crash. By default, RestClient throws an exception for 4xx and 5xx replies. Custom handlers turn those statuses into meaningful messages.

public Weather findByCity(String city) {
    return client.get()
            .uri("/weather/{city}", city)
            .retrieve()
            .onStatus(status -> status.value() == 404,
                    (req, res) -> { throw new CityNotFoundException(city); })
            .onStatus(HttpStatusCode::is5xxServerError,
                    (req, res) -> { throw new ServiceUnavailableException(); })
            .body(Weather.class);
}

Reading Headers and Status

ResponseEntity<Weather> response = client.get()
        .uri("/weather/{city}", city)
        .retrieve()
        .toEntity(Weather.class);

int status = response.getStatusCode().value();
Weather body = response.getBody();

Using WebClient

WebClient needs the spring-boot-starter-webflux dependency. It returns Mono for one item and Flux for many items, and the work runs without blocking a thread.

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

Mono<Weather> result = client.get()
        .uri("/weather/{city}", "Paris")
        .retrieve()
        .bodyToMono(Weather.class);

Call block() on the Mono only in places where blocking is acceptable. A full reactive chain gives the best benefit.

Using RestTemplate

Older projects still use RestTemplate. Reading the code helps during upgrades.

RestTemplate template = new RestTemplate();
Weather weather = template.getForObject(
        "https://api.example.com/weather/{city}", Weather.class, "Paris");

Resilience Tips

 Call fails
    |
    +--> Timeout reached?      --> stop waiting, report error
    +--> Temporary failure?    --> retry a limited number of times
    +--> Service keeps failing? --> use a fallback value

Libraries such as Resilience4j add retry and circuit breaker features on top of any REST client.

Mapping Remote Data

Remote services return JSON that rarely matches your own classes. Create small DTO records that mirror the remote reply, and convert them into your own domain objects inside the service. The remote format can then change without touching the rest of the application. Jackson in Spring Boot ignores unknown fields by default, so new fields added by the partner never break the client.

Best Practices

  • Set connect and read timeouts on every client.
  • Build one client bean per remote service and reuse it.
  • Keep addresses and keys in application.properties instead of source code.
  • Wrap remote calls in a service class so controllers stay simple.
  • Log the status code and duration of each call to track slow partners.
  • Send secrets through headers over HTTPS only.

A well-built REST client makes outside services feel like ordinary Java methods. Clean error handling and sensible timeouts keep your application steady when partners slow down.