Skip to content
Spring Boot sb web 3 min read

WebClient & RestClient

Spring offers two modern HTTP clients with a shared, fluent builder style: WebClient (reactive, non-blocking) and RestClient (synchronous, introduced in Spring Framework 6.1). Both supersede RestTemplate. This page covers building requests, the retrieve and exchange styles, error handling, and timeouts for each.

RestClient — synchronous and modern

RestClient gives you a fluent API with a blocking model, ideal for traditional Spring MVC services. It needs no reactive dependency.

import org.springframework.context.annotation.*;
import org.springframework.web.client.RestClient;

@Configuration
public class HttpClientConfig {

    @Bean
    public RestClient userApiClient(RestClient.Builder builder) {
        return builder
                .baseUrl("https://api.example.com")
                .defaultHeader("Accept", "application/json")
                .build();
    }
}

GET and POST with retrieve:

public record User(Long id, String name, String email) {}
public record CreateUser(String name, String email) {}

@Service
public class UserClient {

    private final RestClient client;

    public UserClient(RestClient userApiClient) {
        this.client = userApiClient;
    }

    public User getUser(Long id) {
        return client.get()
                .uri("/users/{id}", id)
                .retrieve()
                .body(User.class);
    }

    public User create(CreateUser body) {
        return client.post()
                .uri("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .body(body)
                .retrieve()
                .body(User.class);
    }
}

Output:

{ "id": 101, "name": "Grace Hopper", "email": "[email protected]" }

WebClient — reactive and non-blocking

WebClient comes from spring-boot-starter-webflux and returns Mono/Flux. Use it in reactive apps or when you need concurrent, non-blocking calls.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.*;

@Service
public class ReactiveUserClient {

    private final WebClient client;

    public ReactiveUserClient(WebClient.Builder builder) {
        this.client = builder.baseUrl("https://api.example.com").build();
    }

    public Mono<User> getUser(Long id) {
        return client.get()
                .uri("/users/{id}", id)
                .retrieve()
                .bodyToMono(User.class);
    }

    public Flux<User> listUsers() {
        return client.get()
                .uri("/users")
                .retrieve()
                .bodyToFlux(User.class);
    }
}

Tip: You can block a WebClient call with .block(), but if you only ever block, use RestClient instead — it is purpose-built for synchronous code and avoids pulling in the reactive stack.

retrieve vs exchange

retrieve is the concise default for “give me the body or throw on error.” exchange (exchangeToMono / exchange() on RestClient) hands you the full response so you can branch on status before reading the body.

StyleUse when
retrieve()You want the body; errors should throw
exchange() / exchangeToMono()You need to inspect status/headers and decide
// RestClient: inspect status explicitly
User user = client.get()
        .uri("/users/{id}", id)
        .exchange((request, response) -> {
            if (response.getStatusCode().is2xxSuccessful()) {
                return response.bodyTo(User.class);
            }
            throw new UpstreamException("Status " + response.getStatusCode());
        });

Error handling

By default retrieve() throws on 4xx/5xx. Customize per status with onStatus.

// RestClient
public Optional<User> findUser(Long id) {
    User user = client.get()
            .uri("/users/{id}", id)
            .retrieve()
            .onStatus(status -> status.value() == 404, (req, res) -> {
                throw new NotFoundException("User " + id);
            })
            .onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
                throw new UpstreamException("Upstream error " + res.getStatusCode());
            })
            .body(User.class);
    return Optional.ofNullable(user);
}
// WebClient
public Mono<User> getUser(Long id) {
    return client.get()
            .uri("/users/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError,
                    res -> Mono.error(new NotFoundException("User " + id)))
            .bodyToMono(User.class);
}

Timeouts

Configure timeouts on the underlying connector. With RestClient over the JDK or Reactor Netty client:

import io.netty.channel.ChannelOption;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;

HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
        .responseTimeout(Duration.ofSeconds(5));

WebClient webClient = WebClient.builder()
        .baseUrl("https://api.example.com")
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

For reactive flows you can also bound the whole pipeline:

client.get().uri("/users/{id}", id)
        .retrieve()
        .bodyToMono(User.class)
        .timeout(Duration.ofSeconds(5))
        .retry(2);

Warning: Always set connect and response timeouts. Without them a slow upstream can exhaust your connection pool and stall the application.

Choosing a client

ClientModelUse for
RestClientSynchronousNew blocking MVC code (recommended default)
WebClientReactiveWebFlux apps, high-concurrency fan-out
RestTemplateSynchronousLegacy only — maintenance mode

Pitfalls

  • Mixing .block() calls inside a reactive handler defeats non-blocking I/O and can deadlock — keep the chain reactive end-to-end.
  • Reusing one builder across services is fine, but call .build() per distinct configuration; mutating a shared instance is error-prone.
  • Forgetting onStatus means any non-2xx becomes a generic WebClientResponseException you must still translate.
Last updated June 13, 2026
Was this helpful?