Spring Boot Tutorial
HTTP Service Clients with @HttpExchange
Writing RestClient calls by hand for every endpoint of a remote API is repetitive. Spring's HTTP interface clients let you declare a Java interface with annotated methods — much like a Spring Data repository or Feign client — and Spring generates the implementation that makes the HTTP calls.
Spring Framework 7 and Spring Boot 4 make this a first-class feature: @ImportHttpServices registers interface clients as beans, groups them, and configures base URLs and timeouts from spring.http.serviceclient.* properties. This lesson builds typed clients for internal and external services, configures groups, adds authentication, and handles errors.
Declaring an HTTP Interface
Annotate an interface (or its methods) with @HttpExchange and methods with @GetExchange, @PostExchange, @PutExchange, @PatchExchange or @DeleteExchange. Method parameters use the same annotations you know from controllers: @PathVariable, @RequestParam, @RequestHeader, @RequestBody. Return types can be the body type, ResponseEntity<T>, void, or Optional-style wrappers via the adapter.
Registering Clients with @ImportHttpServices
HTTP service clients are part of spring-boot-starter-restclient (or spring-boot-starter-webclient for reactive clients). @ImportHttpServices(group = "catalog", types = CatalogClient.class) (or basePackages = ...) creates a proxy bean for each interface, backed by a RestClient. Clients in the same group share configuration: spring.http.serviceclient.catalog.base-url, connect-timeout, read-timeout and default headers. You no longer need to write HttpServiceProxyFactory boilerplate for each client.
Customising Groups
For authentication, interceptors or custom error handling per group, declare a RestClientHttpServiceGroupConfigurer bean. It receives every group and its RestClient builder, so you can add a bearer token interceptor to the "payments" group only, or an OAuth2 client credentials interceptor to all internal services.
Compared with OpenFeign
Spring Cloud OpenFeign offered the same declarative style for years. HTTP interface clients are built into Spring Framework itself, use RestClient or WebClient underneath, work with virtual threads and reactive types, and need no extra dependency. For new code, prefer HTTP interface clients; Spring Cloud OpenFeign is in maintenance mode.
Examples
Declaring a typed client for a catalogue service
public record Product(Long id, String name, BigDecimal price, int stock) {}
public record NewProduct(String name, BigDecimal price) {}
@HttpExchange("/api/products")
public interface CatalogClient {
@GetExchange
List<Product> list(@RequestParam(required = false) String category,
@RequestParam(defaultValue = "0") int page);
@GetExchange("/{id}")
Product get(@PathVariable long id);
@PostExchange
ResponseEntity<Product> create(@RequestBody NewProduct product);
@PatchExchange("/{id}/stock")
void adjustStock(@PathVariable long id, @RequestParam int delta,
@RequestHeader("X-Request-Id") String requestId);
@DeleteExchange("/{id}")
void delete(@PathVariable long id);
}
(No implementation class — Spring generates a proxy that turns each call into an HTTP request.)
Registering groups and configuring them with properties
@SpringBootApplication
@ImportHttpServices(group = "catalog", types = CatalogClient.class)
@ImportHttpServices(group = "payments", basePackages = "com.webnest.shop.clients.payments")
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
# application.yml
spring:
http:
serviceclient:
catalog:
base-url: http://catalog-service:8081
connect-timeout: 1s
read-timeout: 3s
payments:
base-url: https://api.payments.example.com
read-timeout: 10s
Registered HTTP service client beans: catalogClient (group catalog), paymentGatewayClient (group payments)
Using the client like any other bean
@Service
public class CartService {
private final CatalogClient catalog;
public CartService(CatalogClient catalog) {
this.catalog = catalog;
}
public CartLine addToCart(long productId, int qty) {
Product p = catalog.get(productId);
if (p.stock() < qty) {
throw new OutOfStockException(p.name());
}
catalog.adjustStock(productId, -qty, UUID.randomUUID().toString());
return new CartLine(p.id(), p.name(), qty, p.price());
}
}
GET http://catalog-service:8081/api/products/5
PATCH http://catalog-service:8081/api/products/5/stock?delta=-2 X-Request-Id: 0c9e...
CartLine[productId=5, name=Hoodie, quantity=2, unitPrice=1299.00]
Adding authentication and error handling to one group
@Configuration(proxyBeanMethods = false)
public class HttpServiceGroupsConfig {
@Bean
RestClientHttpServiceGroupConfigurer groupConfigurer(@Value("${payments.api-key}") String apiKey) {
return groups -> groups
.filterByName("payments")
.forEachClient((group, builder) -> builder
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.defaultStatusHandler(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new PaymentRejectedException("Payment API returned " + response.getStatusCode());
}));
}
}
Calls through payment clients carry "Authorization: Bearer ..." and 4xx responses become PaymentRejectedException.
Catalog clients are unaffected.
Common Mistakes
- Hard-coding absolute URLs in @HttpExchange instead of configuring base-url per group, making environments hard to switch.
- Forgetting @ImportHttpServices, then getting "No qualifying bean of type CatalogClient".
- Sharing one group for services with very different timeout and auth requirements.
- Adding Spring Cloud OpenFeign to new projects when built-in HTTP interface clients cover the same need.
- Letting generic HttpClientErrorException leak into business code instead of mapping statuses to domain exceptions.
Key Points to Remember
- Declare remote APIs as interfaces with @HttpExchange and @GetExchange/@PostExchange/etc.
- @ImportHttpServices registers client beans, organised into groups.
- Configure base URLs and timeouts per group with spring.http.serviceclient.<group>.*.
- RestClientHttpServiceGroupConfigurer adds auth headers, interceptors and status handlers per group.
- Built-in HTTP interface clients are the modern replacement for OpenFeign.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.