eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Course – Spring Sale 2026 – NPI (cat=Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

1. Overview

The Spring Framework release 6, as well as Spring Boot version 3, enables us to define declarative HTTP services using Java interfaces. In particular, the approach is inspired by popular HTTP client libraries like Feign and is similar to how we define repositories in Spring Data.

In this tutorial, we’ll first look at how to define an HTTP interface. Then, we’ll check the available exchange method annotations, as well as the supported method parameters and return values. Next, we’ll see how to create an actual HTTP interface instance, a proxy client that performs the declared HTTP exchanges. Moreover, we’ll understand how to perform exception handling and testing of the declarative HTTP interface and its proxy client.

Finally, we examine HTTP service registries as a potential improvement to the usual process.

2. HTTP Interface

The declarative HTTP interface includes annotated methods for HTTP exchanges. We can simply express the remote API details using an annotated Java interface and let Spring generate a proxy that implements this interface and performs the exchanges. This helps reduce the boilerplate code.

2.1. Exchange Methods

@HttpExchange is the root annotation we can add to an HTTP interface and its exchange methods. In case we set it at the interface level, then it applies to all exchange methods. This can be useful for specifying attributes common to all interface methods, like content type or URL prefix.

Of course, additional annotations for all the HTTP methods are available:

  • @GetExchange for HTTP GET requests
  • @PostExchange for HTTP POST requests
  • @PutExchange for HTTP PUT requests
  • @PatchExchange for HTTP PATCH requests
  • @DeleteExchange for HTTP DELETE requests

Let’s define a sample declarative HTTP interface using the method-specific annotations for a simple REST service:

interface BooksService {

    @GetExchange("/books")
    List<Book> getBooks();

    @GetExchange("/books/{id}")
    Book getBook(@PathVariable long id);

    @PostExchange("/books")
    Book saveBook(@RequestBody Book book);

    @DeleteExchange("/books/{id}")
    ResponseEntity<Void> deleteBook(@PathVariable long id);
}

Notably, all the HTTP method-specific annotations are meta-annotated with @HttpExchange. Therefore, @GetExchange(“/books”) is equivalent to @HttpExchange(url = “/books”, method = “GET”).

2.2. Method Parameters

In the example interface, we used @PathVariable and @RequestBody annotations for method parameters. In addition, we may use a set of method parameters for exchange methods:

  • URI: dynamically sets the URL for the request, overriding the annotation attribute
  • HttpMethod: dynamically sets the HTTP method for the request, overriding the annotation attribute
  • @RequestHeader: adds the request header names and values, the argument may be a Map or MultiValueMap
  • @PathVariable: replaces a value that has a placeholder in the request URL
  • @RequestBody: provides the body of the request either as an object to be serialized, or a reactive streams publisher such as Mono or Flux
  • @RequestParam: adds request parameter names and values, the argument may be a Map or MultiValueMap
  • @CookieValue: adds cookie names and values, the argument may be a Map or MultiValueMap

Importantly, request parameters are encoded in the request body only for the content type application/x-www-form-urlencoded. In other cases, request parameters are added as URL query parameters.

2.3. Return Values

Lastly, the exchange methods in the example interface return blocking values. However, declarative HTTP interface exchange methods support both blocking and reactive return values.

In addition, we may choose to return only the specific response information, such as status codes or headers. As well as returning void in case we aren’t interested in the service response at all.

To summarize, HTTP interface exchange methods support a set of return values:

  • void, Mono<Void>: performs the request and releases the response content
  • HttpHeaders, Mono<HttpHeaders>: performs the request, releases the response content, and returns the response headers
  • <T>, Mono<T>: performs the request and decodes the response content to the declared type
  • <T>, Flux<T>: performs the request and decodes the response content to a stream of the declared type
  • ResponseEntity<Void>, Mono<ResponseEntity<Void>>: performs the request, releases the response content, and returns a ResponseEntity containing status and headers
  • ResponseEntity<T>, Mono<ResponseEntity<T>>: performs the request, releases the response content, and returns a ResponseEntity containing status, headers, and the decoded body
  • Mono<ResponseEntity<Flux<T>>: performs the request, releases the response content, and returns a ResponseEntity containing status, headers, and the decoded response body stream

Of course, we can also use any other async or reactive types registered in the ReactiveAdapterRegistry.

3. Client Proxy

Now that we defined a sample HTTP service interface, let’s create a proxy that implements the interface and performs the exchanges.

3.1. Proxy Factory

Spring framework provides an HttpServiceProxyFactory that we can use to generate a client proxy for the HTTP interface:

HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
  .builder(WebClientAdapter.forClient(webClient))
  .build();
booksService = httpServiceProxyFactory.createClient(BooksService.class);

To create a proxy using the provided factory, besides the HTTP interface, we also require an instance of a reactive web client:

WebClient webClient = WebClient.builder()
  .baseUrl(serviceUrl)
  .build();

Now, we can register the client proxy instance as a Spring bean or component and use it to exchange data with the REST service.

3.2. Exception Handling

By default, WebClient throws WebClientResponseException for any client or server error HTTP status codes. In addition, we can customize exception handling by registering a default response status handler that applies to all responses performed through the client:

BooksClient booksClient = new BooksClient(WebClient.builder()
  .defaultStatusHandler(HttpStatusCode::isError, resp ->
    Mono.just(new MyServiceException("Custom exception")))
  .baseUrl(serviceUrl)
  .build());

As a result, in case we request a book that doesn’t exist, we receive a custom exception:

BooksService booksService = booksClient.getBooksService();
assertThrows(MyServiceException.class, () -> booksService.getBook(9));

In this case, we use MyServiceException for a custom implementation.

4. Testing

Let’s see how we can test the sample declarative HTTP interface and its client proxy that performs the exchanges.

4.1. Using Mockito

Since the aim is to test the client proxy created using a declarative HTTP interface, let’s mock the underlying WebClient fluent API using the Mockito deep stubbing feature:

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private WebClient webClient;

Now, let’s use the Mockito BDD methods to call the chained WebClient methods and provide a mocked response:

given(webClient.method(HttpMethod.GET)
  .uri(anyString(), anyMap())
  .retrieve()
  .bodyToMono(new ParameterizedTypeReference<List<Book>>(){}))
  .willReturn(Mono.just(List.of(
    new Book(1,"Book_1", "Author_1", 1998),
    new Book(2, "Book_2", "Author_2", 1999)
  )));

Once we have the mocked response in place, we can call the service using the methods defined in the HTTP interface:

BooksService booksService = booksClient.getBooksService();
Book book = booksService.getBook(1);
assertEquals("Book_1", book.title());

Thus, we verify the result is as expected.

4.2. Using MockServer

In case we want to avoid mocking the WebClient, we can use a library like MockServer to generate and return fixed HTTP responses:

new MockServerClient(SERVER_ADDRESS, serverPort)
  .when(
    request()
      .withPath(PATH + "/1")
      .withMethod(HttpMethod.GET.name()),
    exactly(1)
  )
  .respond(
    response()
      .withStatusCode(HttpStatus.SC_OK)
      .withContentType(MediaType.APPLICATION_JSON)
      .withBody("{\"id\":1,\"title\":\"Book_1\",\"author\":\"Author_1\",\"year\":1998}")
  );

Now that we have the mocked responses in place and a running mock server, let’s call the service:

BooksClient booksClient = new BooksClient(WebClient.builder()
  .baseUrl(serviceUrl)
  .build());
BooksService booksService = booksClient.getBooksService();
Book book = booksService.getBook(1);
assertEquals("Book_1", book.title());

In addition, we can verify that the code under test called the correct mocked service:

mockServer.verify(
  HttpRequest.request()
    .withMethod(HttpMethod.GET.name())
    .withPath(PATH + "/1"),
  VerificationTimes.exactly(1)
);

Again, the verification should pass.

5. HTTP Service Registry

As of Spring Framework 7.0, when working with multiple HTTP interfaces, we can use the HTTP Service Registry to reduce configuration overhead.

Let’s imagine we introduce two other HTTP interfaces:

  1. AuthorsService
  2. PaymentService

Specifically, we can implement them fairly simply:

interface AuthorsService {

    @GetExchange("/authors")
    List<Author> getAuthors();

    @GetExchange("/authors/{id}")
    Author getAuthor(@PathVariable("id") long id);
}

interface PaymentService { 
    // ...
}

Instead of manually creating a @Bean for each of the new service proxies, we can register HTTP services in groups and let Spring handle the proxy creation automatically.

To that end, let’s create a new configuration class and use the @ImportHttpServices annotation to reference the HTTP interfaces:

@Configuration
@ImportHttpServices(group = "books", types = { BooksService.class, AuthorsService.class })
@ImportHttpServices(group = "payments", types = PaymentService.class)
class HttpServicesConfig {

    @Bean
    WebClientHttpServiceGroupConfigurer groupConfigurer() {
        // ...
    }
}

We can now define the RestClientHttpServiceGroupConfigurer and WebClientHttpServiceGroupConfigurer beans to apply custom configuration across entire groups of HTTP interfaces.

For example, to add a default User-Agent header to all HTTP clients, we can configure it once and have it automatically applied to every client, regardless of name or group:

@Bean
WebClientHttpServiceGroupConfigurer groupConfigurer() {
    return groups -> {
        groups.forEachClient((client, builder) -> builder
          .defaultHeader("User-Agent", "Baeldung-Client v1.0"));
    };
}

On the other hand, if we need a specific configuration for certain groups, such as the “books” group containing BookService and AuthorService, we can filter by group name:

@Bean
WebClientHttpServiceGroupConfigurer groupConfigurer() {
    return groups -> {
        groups.forEachClient((group, builder) -> builder
          .defaultHeader("User-Agent", "Baeldung-Client v1.0"));

        groups.filterByName("books")
          .forEachClient((group, builder) -> builder
            .defaultUriVariables(Map.of("foo", "bar"))
            .defaultApiVersion("v1"));
    };
}

As we can see, the WebClientHttpServiceGroupConfigurer provides a fluent API for configuring HTTP clients at multiple levels: globally across all interfaces, for specific groups, or for individual HTTP interfaces.

6. Conclusion

In this article, we explored declarative HTTP service interfaces available in Spring release 6. We looked at how to define an HTTP interface using the available exchange method annotations, as well as the supported method parameters and return values.

Further, we explored how to create a proxy client that implements the HTTP interface and performs the exchanges. Also, we performed exception handling by defining a custom status handler. On top of that, we saw how to test the declarative interface and its client proxy using Mockito and MockServer. Finally, we leveraged an HTTP service registry and understood how it works.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – Spring Sale 2026 – NPI EA (cat= Baeldung)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

Course – Spring Sale 2026 – NPI (All)
announcement - icon

Yes, we're now running our Spring Sale. All Courses are 30% off until 31st March, 2026

>> EXPLORE ACCESS NOW

eBook – Mockito – NPI (tag=Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

>> Download the eBook

eBook Jackson – NPI EA – 3 (cat = Jackson)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments