Java

Mastering Declarative HTTP Interfaces with WebClient in Spring 6

Declarative HTTP Interfaces, introduced in Spring Framework 6, are a strong feature that streamlines API interactions by removing boilerplate code. Instead of manually using WebClient in services, you may now define HTTP operations using annotated interfaces, making code easier to maintain and test.
In this blog post, we will examine how to use Declarative HTTP Interfaces in Spring 6 using a real-world example.

What Are Declarative HTTP Interfaces?

Declarative HTTP interfaces allow you to define HTTP clients by annotating Java interfaces with @HttpExchange. Spring automatically implements these interfaces with WebClient. This method is similar to Feign clients in Spring Cloud, but it is now a built-in part of Spring Framework 6.

Setting Up WebClient with Declarative HTTP Interface

Let's look at a real-world example of consuming a REST API using Spring 6's declarative approach.

Dependencies Used

To use WebClient and declarative HTTP interfaces, ensure you have the following dependencies in your pom.xml:

<dependencies>
    <!-- Spring WebFlux for reactive WebClient -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <!-- Lombok for reducing boilerplate code -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- Spring Boot Starter for Spring applications -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>

1. Define the HTTP Client Interface

We create the PostClient interface to interact with a JSONPlaceholder API that generates dummy posts.

package com.example.demo.client;

import com.example.demo.model.Post;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

import java.util.List;

@HttpExchange(accept = "application/json", url = "/posts")
public interface PostClient {

    @GetExchange
    List<Post> getAllPost();

    @GetExchange("/{postId}")
    Post getPostById(@PathVariable("postId") int postId);
}

In this interface:

  • @HttpExchange sets the base URL path (/posts).
  • @GetExchange defines GET requests to fetch all posts orspecific post by ID.

2. Configure WebClient and Register the HTTP Client

We now configure WebClient and register PostClient as a bean.

package com.example.demo.config;

import com.example.demo.client.PostClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.service.invoker.WebClientAdapter;

@Configuration
public class PostConfig {

    @Bean
    WebClient webClient() {
        return WebClient.builder()
                .baseUrl("https://jsonplaceholder.typicode.com")
                .build();
    }

    @Bean
    public PostClient postClient() {
        WebClientAdapter webClientAdapter = WebClientAdapter.create(webClient());
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(webClientAdapter).build();
        return factory.createClient(PostClient.class);
    }
}

Here’s what’s happening:

  • We create a WebClient bean with the base URL (jsonplaceholder.typicode.com).
  • We use WebClientAdapter and HttpServiceProxyFactory to instantiate PostClient dynamically.

3. Implement a Service Layer

Now, let’s create a service class PostService to use PostClient for fetching posts.

package com.example.demo.service;

import com.example.demo.client.PostClient;
import com.example.demo.model.Post;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostClient postClient;

    public List<Post> getPosts() {
        return postClient.getAllPost();
    }

    public Post getPostById(int id) {
        return postClient.getPostById(id);
    }
}

4. Expose an API Endpoint

We need a controller to expose these services via a REST API.

package com.example.demo.controller;

import com.example.demo.model.Post;
import com.example.demo.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("api/v1/post")
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    @GetMapping
    public List<Post> getPosts() {
        return postService.getPosts();
    }

    @GetMapping("/{id}")
    public Post getPostById(@PathVariable int id) {
        return postService.getPostById(id);
    }
}

5. Define the Post Model

Finally, we define a Post model to map the JSON response.

package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Post {
    private Integer id;
    private String title;
    private String body;
    private Integer userId;
}

Running the Application

Once the application is running, you can test the API:

  • Fetch all posts: GET http://localhost:8080/api/v1/post
  • Fetch a post by ID: GET http://localhost:8080/api/v1/post/1

Benefits of Declarative HTTP Interfaces in Spring 6

  • Less Boilerplate Code: No need to manually handle WebClient in services.
  • Improved Readability: API definitions are clear and concise.
  • Better Maintainability: Interface-based API clients simplify testing and updates.
  • Spring Native Support: No need for additional dependencies like Feign.

Bonus

If you encounter a certificate error, you can work around it by modifying the WebClient bean.

    WebClient webClient() throws Exception {
        // Create SSL context that accepts all certificates
        SslContext sslContext = SslContextBuilder
                .forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build();

        // Create HttpClient with the SSL context
        HttpClient httpClient = HttpClient.create()
                .secure(t -> t.sslContext(sslContext));

        // Create WebClient with the configured HttpClient
        return WebClient.builder()
                .baseUrl("https://jsonplaceholder.typicode.com")
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

Conclusion

Spring 6’s Declarative HTTP Interfaces simplify API communication with a clean, interface-driven approach. By using @HttpExchange with WebClient, you can efficiently consume REST APIs with minimal code.

Try implementing this feature in your Spring projects and experience the benefits firsthand!

About

At DevelopersMonk, we share tutorials, tips, and insights on modern programming frameworks like React, Next.js, Spring Boot, and more. Join us on our journey to simplify coding and empower developers worldwide!

Email: developersmonks@gmail.com

Phone: +23359*******

Quick Links

  • Home
  • About
  • Contact Us
  • Categories

  • Java
  • TypeScript
  • JavaScript
  • React
  • Nextjs
  • Spring Boot
  • DevOps