Home / Spring Boot / Web and REST / Calling Other APIs: RestClient and WebClient

Calling Other APIs: RestClient and WebClient

Most applications call other services. Spring gives you RestClient (modern, blocking, fluent), WebClient (reactive) and declarative HTTP interfaces. RestTemplate still works but is in maintenance mode.

Which client should I use?

ClientStyleUse when
RestClient (Spring 6.1, Boot 3.2+)Blocking, fluent APIDefault choice in a normal Spring MVC application
WebClientNon-blocking, reactiveReactive applications or streaming
HTTP interface (@HttpExchange)Declarative, like FeignYou want a typed client from an interface
RestTemplateBlocking, older APIExisting code only. Avoid in new code

RestClient configuration and use

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

public record GitHubUser(String login, String name) { }

@Configuration
public class ClientConfig {
    @Bean
    public RestClient githubClient(RestClient.Builder builder) {
        return builder
                .baseUrl("https://api.github.com")
                .defaultHeader("Accept", "application/vnd.github+json")
                .build();
    }
}

@Service
public class GitHubService {
    private final RestClient client;

    public GitHubService(RestClient githubClient) { this.client = githubClient; }

    public GitHubUser getUser(String username) {
        return client.get()
                .uri("/users/{u}", username)
                .retrieve()
                .onStatus(status -> status.value() == 404,
                        (req, res) -> { throw new NotFoundException("No GitHub user " + username); })
                .body(GitHubUser.class);
    }

    public String createIssueExample(Object payload) {
        return client.post()
                .uri("/repos/owner/repo/issues")
                .contentType(MediaType.APPLICATION_JSON)
                .body(payload)
                .retrieve()
                .body(String.class);
    }
}

Timeouts (Boot 3.4 and newer)

spring.http.client.connect-timeout=2s
spring.http.client.read-timeout=5s

Always set timeouts. A call without a timeout can block a thread for a very long time when the other service is slow.

Declarative HTTP interface

import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@HttpExchange("/users")
public interface UserClient {
    @GetExchange("/{login}")
    GitHubUser get(@PathVariable String login);
}

@Configuration
public class HttpInterfaceConfig {
    @Bean
    public UserClient userClient(RestClient githubClient) {
        HttpServiceProxyFactory factory =
                HttpServiceProxyFactory.builderFor(RestClientAdapter.create(githubClient)).build();
        return factory.createClient(UserClient.class);
    }
}

WebClient (reactive)

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

Mono<GitHubUser> user = client.get().uri("/users/{u}", "octocat")
        .retrieve()
        .bodyToMono(GitHubUser.class);
// user.block() waits for the result. Avoid block() inside reactive code.

What is inside a RestClient call

client.get().uri("/users/{u}", "ram").retrieve().body(User.class)
   1. build URI (UriBuilderFactory, encoding of {u})
   2. ClientHttpRequestInterceptors (your logging, tracing, auth headers) run in order
   3. ClientHttpRequestFactory creates the request:  JDK HttpClient, Apache HttpClient 5, Jetty, or Reactor Netty (auto-detected from the classpath)
   4. send, wait for the response
   5. status handlers (onStatus) decide about errors. Default: 4xx -> RestClientResponseException (HttpClientErrorException), 5xx -> HttpServerErrorException
   6. HttpMessageConverters read the body (Jackson) into the target type

Interceptor: add auth header and log timing

@Configuration
public class ClientConfig {

    @Bean
    RestClient partnerClient(RestClient.Builder builder, @Value("${partner.token}") String token) {
        return builder
                .baseUrl("https://partner.example.com")
                .requestInterceptor((request, body, execution) -> {
                    request.getHeaders().setBearerAuth(token);
                    long start = System.nanoTime();
                    ClientHttpResponse response = execution.execute(request, body);
                    System.out.println(request.getMethod() + " " + request.getURI() + " -> "
                            + response.getStatusCode() + " in " + (System.nanoTime() - start) / 1_000_000 + " ms");
                    return response;
                })
                .build();
    }
}

Timeouts per client with the request factory

@Bean
RestClient slowPartnerClient(RestClient.Builder builder) {
    ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults()
            .withConnectTimeout(Duration.ofSeconds(2))
            .withReadTimeout(Duration.ofSeconds(10));
    return builder.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings)).baseUrl("https://slow.example.com").build();
}

Error handling patterns

public Optional<User> find(String id) {
    try {
        return Optional.ofNullable(client.get().uri("/users/{id}", id).retrieve().body(User.class));
    } catch (HttpClientErrorException.NotFound e) {
        return Optional.empty();                                            // 404 is a normal answer here
    } catch (HttpClientErrorException e) {
        throw new PartnerBadRequestException(e.getStatusCode(), e.getResponseBodyAsString());
    } catch (ResourceAccessException e) {
        throw new PartnerUnavailableException("timeout or connection problem", e);   // I/O error: retry candidates
    }
}

// exchange() gives the raw response when you need status, headers and body together
ResponseEntity<User> entity = client.get().uri("/users/{id}", id).retrieve().toEntity(User.class);

Testing a client without the network

@RestClientTest(GitHubService.class)
class GitHubServiceTest {

    @Autowired GitHubService service;
    @Autowired MockRestServiceServer server;

    @Test
    void returnsUser() {
        server.expect(requestTo("https://api.github.com/users/octocat"))
              .andRespond(withSuccess("{\"login\":\"octocat\",\"name\":\"The Octocat\"}", MediaType.APPLICATION_JSON));

        assertThat(service.getUser("octocat").login()).isEqualTo("octocat");
    }

    @Test
    void mapsNotFound() {
        server.expect(requestTo("https://api.github.com/users/nobody")).andRespond(withStatus(HttpStatus.NOT_FOUND));
        assertThrows(NotFoundException.class, () -> service.getUser("nobody"));
    }
}
// For complex scenarios (delays, faults, stateful stubs) use WireMock or a MockWebServer.
ClientStyleBest for
RestClientBlocking, fluent, modernDefault in MVC apps
WebClientReactiveReactive stacks, streaming
HTTP interface (@HttpExchange)Declarative proxy over RestClient or WebClientTyped clients, like Feign
OpenFeignDeclarative (Spring Cloud)Existing Spring Cloud systems
JDK HttpClientLow levelLibraries without Spring

Common mistake: Calling another service without timeouts and without error handling. A slow dependency then blocks all your threads. Set timeouts, handle errors and consider a circuit breaker such as Resilience4j.

Interview questions

Is RestTemplate deprecated?

It is in maintenance mode: it still works but gets no new features. Use RestClient for new code.

Can I use WebClient in a Spring MVC application?

Yes, but you must call block() to get results, and you need the webflux dependency. RestClient is simpler for blocking code.

How do I retry failed calls?

Use Spring Framework 7 retry support, Spring Retry, or Resilience4j, and retry only safe (idempotent) requests.

How do I add a correlation id to every outgoing request?

Add a ClientHttpRequestInterceptor that copies the MDC value into a header. Boot's tracing support does it automatically for builders created by Spring.

Which HTTP library does RestClient use under the hood?

It auto-detects: Apache HttpClient 5, Jetty client, Reactor Netty, or the JDK HttpClient. You can force one with spring.http.client.factory.