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?
| Client | Style | Use when |
|---|---|---|
| RestClient (Spring 6.1, Boot 3.2+) | Blocking, fluent API | Default choice in a normal Spring MVC application |
| WebClient | Non-blocking, reactive | Reactive applications or streaming |
| HTTP interface (@HttpExchange) | Declarative, like Feign | You want a typed client from an interface |
| RestTemplate | Blocking, older API | Existing 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=5sAlways 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 typeInterceptor: 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.| Client | Style | Best for |
|---|---|---|
| RestClient | Blocking, fluent, modern | Default in MVC apps |
| WebClient | Reactive | Reactive stacks, streaming |
| HTTP interface (@HttpExchange) | Declarative proxy over RestClient or WebClient | Typed clients, like Feign |
| OpenFeign | Declarative (Spring Cloud) | Existing Spring Cloud systems |
| JDK HttpClient | Low level | Libraries 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.