CoApi - HTTP Client that supports both reactive programming and synchronous programming models
May 12, 2026 · View on GitHub
In Spring Framework 6, a new HTTP
client, Spring6 HTTP Interface,
has been introduced. This interface allows developers to define HTTP services as Java interfaces using the
@HttpExchange annotation.
However, the current Spring ecosystem does not yet provide support for automatic configuration, and developers need to implement the configuration themselves.
While the Spring ecosystem already has Spring Cloud OpenFeign, it lacks support for the reactive programming model. To address this, Spring Cloud OpenFeign recommends an alternative solution, feign-reactive. However, this alternative is currently not actively maintained and does not support Spring Boot 3.2.x.
CoApi is here to help with zero-boilerplate code auto-configuration similar to Spring Cloud OpenFeign, as well as support for both reactive and synchronous programming models. Developers only need to define the interface, and it is easy to use.
Spring Boot Version Compatibility
CoApi 1.x supports Spring Boot 3.2.x
CoApi 2.x supports Spring Boot 4.x
Installation
Use Gradle(Kotlin) to install dependencies
implementation("me.ahoo.coapi:coapi-spring-boot-starter")
Use Gradle(Groovy) to install dependencies
implementation 'me.ahoo.coapi:coapi-spring-boot-starter'
Use Maven to install dependencies
<dependency>
<groupId>me.ahoo.coapi</groupId>
<artifactId>coapi-spring-boot-starter</artifactId>
<version>${coapi.version}</version>
</dependency>
Usage
Define CoApi - a third-party interface
baseUrl: Define the base address of the request, which can be obtained from the configuration file, for example:baseUrl = "${github.url}",github.urlis the configuration item in the configuration file
@CoApi(baseUrl = "${github.url}")
public interface GitHubApiClient {
@GetExchange("repos/{owner}/{repo}/issues")
Flux<Issue> getIssue(@PathVariable String owner, @PathVariable String repo);
}
Configuration:
github:
url: https://api.github.com
Define CoApi - Client Load Balancing
When using client load balancing, you need to introduce dependencies first:
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
- Use the
serviceIdparameter definition:
@CoApi(serviceId = "github-service")
public interface ServiceApiClient {
@GetExchange("repos/{owner}/{repo}/issues")
Flux<Issue> getIssue(@PathVariable String owner, @PathVariable String repo);
}
- Client load balancing protocol (
lb://) definition viabaseUrlparameter:
@CoApi(baseUrl = "lb://github-service")
public interface ServiceApiClient {
}
Using CoApi
@RestController
class GithubController(
private val gitHubApiClient: GitHubApiClient,
private val serviceApiClient: ServiceApiClient
) {
@GetMapping("/baseUrl")
fun baseUrl(): Flux<Issue> {
return gitHubApiClient.getIssue("Ahoo-Wang", "CoApi")
}
@GetMapping("/serviceId")
fun serviceId(): Flux<Issue> {
return serviceApiClient.getIssue("Ahoo-Wang", "CoApi")
}
}
Case Reference
Service Provider
classDiagram
direction BT
class TodoApi {
<<Interface>>
}
class TodoClient {
<<Interface>>
}
class TodoController
TodoClient --> TodoApi
TodoController ..> TodoApi
TodoApi: A common contract between the client consumer and the service provider is defined to prevent the risk of duplicate redundant definitions and to eliminate inconsistencies between the service provider implementation and the client SDK.TodoClient:The client consumer accesses the service provider's API viaTodoClient.TodoController: The service provider is responsible for implementing theTodoApiinterface.
Define API
@HttpExchange("todo")
interface TodoApi {
@GetExchange
fun getTodo(): Flux<Todo>
}
Define Client
@CoApi(serviceId = "provider-service")
interface TodoClient : TodoApi
Implement API
@RestController
class TodoController : TodoApi {
override fun getTodo(): Flux<Todo> {
return Flux.range(1, 10)
.map {
Todo("todo-$it")
}
}
}
Service Consumer
The service consumer turns on the automatic configuration of the CoApi via the @EnableCoApi annotation.
@EnableCoApi(clients = [TodoClient::class])
@SpringBootApplication
class ConsumerServer
@RestController
class TodoController(private val todoClient: TodoClient) {
@GetExchange
fun getProviderTodo(): Flux<Todo> {
return todoClient.getTodo()
}
}