Here’s the English translation:
Whenever you call an external API, writing out RestClient or WebClient code over and over tends to clutter things up with URL assembly and parameter passing. Have you ever thought, “I’d like to just write an interface like OpenFeign does, but pulling in spring-cloud-openfeign just for that feels a bit heavy”?
That’s where HTTP Interface (@HttpExchange), included by default since Spring Framework 6, comes in. With zero additional dependencies, you can create a declarative HTTP client just by defining an interface. In this article, we’ll build everything in a working form—from interface definition to Proxy generation to error handling.
What Is HTTP Interface
HTTP Interface is a mechanism that lets you declaratively define an HTTP client simply by adding annotations to a Java interface. You don’t write the implementation class yourself. Spring generates a Proxy at runtime that converts method calls into actual HTTP requests.
The key point is that this is designed to sit on top of RestClient / WebClient / RestTemplate as an adapter. In other words, the underlying HTTP communication is handled by the same clients as before, and you’re simply adding a declarative layer in front of them.
The biggest difference from OpenFeign is the dependencies. OpenFeign requires spring-cloud-openfeign, which is part of Spring Cloud, whereas HTTP Interface is a feature of Spring Framework itself, so you can use it as-is as long as you have the Web starter.
Prerequisites and Dependencies
You need Spring Framework 6 or higher—that is, Spring Boot 3 or higher. It’s not available in version 5 or earlier. For synchronous use, add spring-boot-starter-web; for reactive, add spring-boot-starter-webflux.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
In this article, we’ll focus on the synchronous RestClientAdapter. Just note that because RestClientAdapter uses RestClient, it requires Spring Boot 3.2 or later. We’ll cover the reactive WebClientAdapter later on.
Defining the Client Interface
First, express the API you want to call as an interface. Here, let’s use an API that handles user information as an example.
import org.springframework.web.service.annotation.*;
import org.springframework.web.bind.annotation.*;
@HttpExchange("/api/users")
public interface UserApiClient {
@GetExchange("/{id}")
User getUser(@PathVariable Long id);
@GetExchange
List<User> searchUsers(@RequestParam String keyword);
@PostExchange
User createUser(@RequestBody UserRequest request,
@RequestHeader("X-Tenant-Id") String tenantId);
}
Declare the base path with @HttpExchange, and add @GetExchange or @PostExchange to each method. Parameter binding uses the same familiar annotations: @PathVariable corresponds to path variables, @RequestParam to query parameters, @RequestBody to the request body, and @RequestHeader to individual headers.
Return types are flexible too. If you return an object directly like User, the body is deserialized; if you return List<User>, you get a collection. If you also want to inspect the status code or headers, return a ResponseEntity<User>.
Generating the Proxy with HttpServiceProxyFactory
Once the interface is ready, create the actual Proxy from it. This is where HttpServiceProxyFactory comes in. Wrap the RestClient with RestClientAdapter, and build the factory from that.
@Configuration
public class HttpClientConfig {
@Bean
public UserApiClient userApiClient() {
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(UserApiClient.class);
}
}
factory.createClient(UserApiClient.class) returns the Proxy implementation, so all you need to do is register it as a Bean. After that, you can DI it and use it normally from anywhere in your application.
@Service
public class UserService {
private final UserApiClient client;
public UserService(UserApiClient client) {
this.client = client;
}
public User find(Long id) {
return client.getUser(id);
}
}
The trick is to consolidate the base URL and shared configuration on the RestClient that the adapter is based on. Keeping the interface focused on “what to call,” and pushing “where and how to call it” toward the client side, makes things much clearer.
Configuring the Underlying Client
In real-world operation, you’ll want to configure not just the base URL but also common headers and timeouts all in one place. These can all be specified on the underlying RestClient.
@Bean
public RestClient apiRestClient(ApiProperties props) {
var settings = ClientHttpRequestFactorySettings.defaults()
.withConnectTimeout(Duration.ofSeconds(3))
.withReadTimeout(Duration.ofSeconds(10));
return RestClient.builder()
.baseUrl(props.baseUrl())
.defaultHeader("Authorization", "Bearer " + props.token())
.requestFactory(ClientHttpRequestFactoryBuilder.detect().build(settings))
.build();
}
When you want to switch the base URL per environment, externalizing it with @ConfigurationProperties makes it easier to handle.
@ConfigurationProperties(prefix = "api")
public record ApiProperties(String baseUrl, String token) {}
With this setup, multiple HTTP Interfaces can share the same RestClient, and you can prevent mistakes such as forgetting to attach authentication headers.
Error Handling (4xx / 5xx)
By default, an exception is thrown for 4xx / 5xx responses, but you’ll often want to convert it into a custom exception. Using RestClient’s defaultStatusHandler, you can insert cross-cutting handling for each status.
RestClient.builder()
.baseUrl(props.baseUrl())
.defaultStatusHandler(HttpStatusCode::is4xxClientError, (req, res) -> {
ApiError error = readError(res);
throw new ApiClientException(res.getStatusCode(), error);
})
.defaultStatusHandler(HttpStatusCode::is5xxServerError, (req, res) -> {
throw new ApiServerException(res.getStatusCode());
})
.build();
If the error response body contains a detailed message, deserializing it into a dedicated DTO makes it easier for the caller to identify the cause.
When using WebClient, onStatus plays the same role. The syntax differs, but the idea of branching on a status condition and throwing an exception is the same.
Using It Reactively with the WebClient Adapter
If you want to make non-blocking calls on the WebFlux stack, use WebClientAdapter.
@Bean
public UserApiClient reactiveUserApiClient(WebClient.Builder builder) {
WebClient webClient = builder.baseUrl("https://api.example.com").build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient))
.build();
return factory.createClient(UserApiClient.class);
}
You just swap out the adapter, and the configuration flow is almost identical. For methods you want to make reactive, set the return type to Mono<User> or Flux<User> and they’ll run non-blocking.
The selection criterion is simple: if your entire application is built on WebFlux, choose WebClient; otherwise, just go with RestClient. There’s no need to force reactive into a synchronous stack.
Comparison with OpenFeign and When to Use Each
Both are similar in that they’re declarative, but their positioning is quite different.
| Aspect | @HttpExchange | OpenFeign |
|---|---|---|
| Dependency | Standard in Spring Framework (zero additions) | Requires spring-cloud-openfeign |
| Underlying client | RestClient / WebClient | Proprietary (integratable) |
| Service discovery integration | Not supported out of the box | Easy to integrate with Eureka, etc. |
| Load balancer integration | Configured separately | Integrates with Spring Cloud LoadBalancer |
Roughly speaking, if you want to leverage the Spring Cloud ecosystem (service discovery and client-side load balancing), OpenFeign has the advantage. On the other hand, for simple external API integration or cases where you don’t want to add dependencies, @HttpExchange fits cleanly. For detailed OpenFeign configuration, see Building a Declarative HTTP Client with OpenFeign in Spring Boot.
Note that resilience features like retries and circuit breakers are handled by separate mechanisms in either approach. If you want to combine them with @HttpExchange, Implementing a Resilience4j Circuit Breaker in Spring Boot is a good reference. If you want to dig into the underlying adapter client itself, also check out Implementing a Synchronous HTTP Client with RestClient in Spring Boot.
Summary
With @HttpExchange and HttpServiceProxyFactory, you can create a declarative HTTP client without adding Spring Cloud dependencies. The flow was a four-step process: define the interface, generate the Proxy via RestClientAdapter, consolidate the base URL and shared configuration on the underlying client, and clean up errors with defaultStatusHandler.
It’s also satisfying that you can switch by simply choosing the adapter—RestClient for synchronous, WebClient for reactive. Starting with a small external API integration and replacing it with a single interface is a great way to feel the benefits firsthand.