Mastering Spring Boot 4 Declarative HTTP Client: A Complete Practical Guide

This article walks through Spring Boot 4's built‑in HTTP Interface, showing how to replace RestTemplate/WebClient with a type‑safe, annotation‑driven RestClient, covering Maven setup, DTO and interface definitions, configuration, error handling, header injection, timeout control, reactive alternatives, design principles, and a side‑by‑side comparison with the legacy approach.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Mastering Spring Boot 4 Declarative HTTP Client: A Complete Practical Guide

Declarative HTTP Client in Spring Boot 4

Define a Java interface with annotations; Spring generates the implementation at runtime.

Core annotations: @HttpExchange, @GetExchange, @PostExchange, @PutExchange, @DeleteExchange. Spring Boot 4 binds them to the synchronous RestClient, eliminating manual URL construction, header setting, JSON parsing, and repetitive error‑handling code.

Project Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

Configuration Property

todo.api.base-url=https://jsonplaceholder.typicode.com

Step 1 – DTO

package com.icoderoad.todo.model;

public record Todo(Long userId, Long id, String title, boolean completed) {}

Step 2 – Declarative HTTP Interface

package com.icoderoad.todo.client;

import java.util.List;
import com.icoderoad.todo.model.Todo;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.*;

@HttpExchange("/todos")
public interface TodoClient {
    @GetExchange
    List<Todo> findAll();

    @GetExchange("/{id}")
    Todo findById(@PathVariable Long id);

    @PostExchange
    Todo create(@RequestBody Todo newTodo);

    @PutExchange("/{id}")
    Todo update(@PathVariable Long id, @RequestBody Todo updatedTodo);

    @DeleteExchange("/{id}")
    void delete(@PathVariable Long id);
}

Step 3 – RestClient Configuration

package com.icoderoad.todo.config;

import com.icoderoad.todo.client.TodoClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class TodoClientConfig {

    @Bean
    public RestClient todoRestClient(RestClient.Builder builder,
                                     @Value("${todo.api.base-url}") String baseUrl) {
        return builder.baseUrl(baseUrl)
                      .defaultHeader("Accept", "application/json")
                      .build();
    }

    @Bean
    public TodoClient todoClient(RestClient restClient) {
        var adapter = RestClientAdapter.create(restClient);
        var factory = HttpServiceProxyFactory.builder(adapter).build();
        return factory.createClient(TodoClient.class);
    }
}

Flow:

RestClient → RestClientAdapter → HttpServiceProxyFactory → generated TodoClient proxy

.

Step 4 – Service Layer

package com.icoderoad.todo.service;

import com.icoderoad.todo.client.TodoClient;
import com.icoderoad.todo.model.Todo;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class TodoService {
    private final TodoClient client;
    public TodoService(TodoClient client) { this.client = client; }

    public List<Todo> getAll() { return client.findAll(); }
    public Todo getOne(Long id) { return client.findById(id); }
    public Todo create(Todo todo) { return client.create(todo); }
    public Todo update(Long id, Todo todo) { return client.update(id, todo); }
    public void delete(Long id) { client.delete(id); }
}

Step 5 – Controller

package com.icoderoad.todo.controller;

import com.icoderoad.todo.model.Todo;
import com.icoderoad.todo.service.TodoService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class TodoController {
    private final TodoService service;
    public TodoController(TodoService service) { this.service = service; }

    @GetMapping
    public List<Todo> list() { return service.getAll(); }

    @GetMapping("/{id}")
    public Todo detail(@PathVariable Long id) { return service.getOne(id); }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Todo create(@RequestBody Todo todo) { return service.create(todo); }

    @PutMapping("/{id}")
    public Todo update(@PathVariable Long id, @RequestBody Todo todo) { return service.update(id, todo); }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void remove(@PathVariable Long id) { service.delete(id); }
}

Remote Error Handling

@Bean
public TodoClient todoClient(RestClient restClient) {
    var adapter = RestClientAdapter.create(restClient);
    var factory = HttpServiceProxyFactory.builder(adapter)
        .statusHandler(status -> status.is4xxClientError(),
            (request, response) -> {
                throw new IllegalStateException("Remote API 4xx error: " + response.getStatusCode());
            })
        .build();
    return factory.createClient(TodoClient.class);
}

This converts 4xx responses into business exceptions and hides low‑level HTTP details.

Custom Header and Additional Configuration

@Bean
public RestClient todoRestClient(RestClient.Builder builder,
                                 @Value("${todo.api.base-url}") String baseUrl) {
    return builder.baseUrl(baseUrl)
                  .defaultHeader("Accept", "application/json")
                  .defaultHeader("X-App-Source", "icoderoad-service")
                  .build();
}

Further customization can include connection pooling, timeout settings, or switching the underlying client (Apache HttpClient, Reactor Netty).

Reactive Variant

@HttpExchange("/todos")
public interface ReactiveTodoClient {
    @GetExchange
    Flux<Todo> findAll();

    @GetExchange("/{id}")
    Mono<Todo> findById(@PathVariable Long id);
}

When using WebFlux, replace RestClientAdapter with WebClientAdapter; methods return Mono or Flux.

Design Guidelines

One interface per remote service.

DTOs expose only required fields.

Controllers never depend directly on the client; they call a Service layer.

Layering: Controller → Service → HTTP Interface → Remote API improves decoupling, testability, and replaceability.

Comparison with Legacy Approaches

Boilerplate code: many vs very few.

Readability: average vs very clear.

Type safety: average vs strong.

Third‑party dependency: OpenFeign vs none.

Official support: RestTemplate deprecated vs HTTP Interface officially recommended.

Conclusion

For projects migrating from RestTemplate or Feign, the Spring Boot 4 + Spring Framework 6 HTTP Interface provides type safety, minimal boilerplate, and deep integration with the Spring ecosystem.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavamicroservicesSpring BootRestClienthttp-interfacedeclarative-clientspring-boot-4
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.