From ahoo-skills
Help developers use the CoApi library — a Spring Framework HTTP client auto-configuration library that supports both reactive (WebClient) and synchronous (RestClient) programming models. Use this skill whenever the user mentions CoApi, @CoApi, @HttpExchange, HTTP client interface, Spring HTTP Interface, or wants to create/modify/test HTTP client proxies. Also trigger when users ask about Spring 6 HTTP Interface auto-configuration, load-balanced HTTP clients, switching between reactive and sync HTTP client modes, or reference classes like CoApiFactoryBean, CoApiDefinition, ClientMode, WebClientFactoryBean, RestClientFactoryBean, HttpExchangeAdapterFactory, AutoCoApiRegistrar, or configuration properties like coapi.mode, coapi.clients, coapi.base-packages. Make sure to use this skill even if the user doesn't explicitly mention CoApi by name but is working with Spring HTTP Interface proxies, declaring annotated HTTP client interfaces, or configuring Spring Boot auto-configuration for HTTP exchanges.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ahoo-skills:coapi-developerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are helping a developer use the **CoApi** library (`me.ahoo.coapi`), which provides zero-boilerplate auto-configuration for Spring 6 HTTP Interface clients (`@HttpExchange`). It supports both reactive (`WebClient`) and synchronous (`RestClient`) modes, with optional client-side load balancing via Spring Cloud LoadBalancer.
You are helping a developer use the CoApi library (me.ahoo.coapi), which provides zero-boilerplate auto-configuration for Spring 6 HTTP Interface clients (@HttpExchange). It supports both reactive (WebClient) and synchronous (RestClient) modes, with optional client-side load balancing via Spring Cloud LoadBalancer.
CoApi targets Spring Boot 4.x / Spring Framework 7.x with Kotlin (JVM 17).
Maven coordinates:
implementation("me.ahoo.coapi:coapi-spring-boot-starter")
Note: The Maven artifact ID is coapi-spring-boot-starter (not spring-boot-starter). The module name in the project is spring-boot-starter, but the published artifact follows the pattern coapi-<module-name> via getArchivesName().
Key annotations:
@CoApi(baseUrl | serviceId, name) — marks an interface as an HTTP client proxy@LoadBalanced — enables load balancing for the client@EnableCoApi(clients = [...]) — explicitly registers client interfaces (alternative to auto-scanning)Configuration property: coapi.mode = AUTO | REACTIVE | SYNC
CoApi is designed specifically for declarative HTTP Interface clients. It is not the right choice for:
WebSocketClient or WebClient directly.WebClient or RestClient, so resilience features belong in filters/interceptors (e.g., Resilience4j). CoApi configures the wiring, not the resilience policy.HttpServiceProxyFactory directly.RestClient or WebClient directly. CoApi adds value when you have a typed interface contract.Use standard Spring @HttpExchange annotations on the interface. A class-level @HttpExchange sets a base path for all methods — this is useful for grouping endpoints under a common prefix. Return types determine the programming model:
Flux<T>, Mono<T>, or any Publisher<T>List<T>, T, void)@HttpExchange("todo")
interface TodoApi {
@GetExchange
fun getTodo(): Flux<Todo>
}
data class Todo(val title: String)
The @CoApi annotation goes on the client interface. There are several ways to specify the target:
Direct URL (with optional placeholder):
@CoApi(baseUrl = "\${github.url}")
interface GitHubApiClient {
@GetExchange("repos/{owner}/{repo}/issues")
fun getIssue(@PathVariable owner: String, @PathVariable repo: String): Flux<Issue>
}
Service ID (load-balanced):
When you set serviceId, CoApi automatically constructs an lb://serviceId URL. This tells Spring Cloud LoadBalancer to resolve the actual host from the service registry — you don't need to know the concrete URL at development time.
@CoApi(serviceId = "github-service")
interface ServiceApiClient {
@GetExchange("repos/{owner}/{repo}/issues")
fun getIssue(@PathVariable owner: String, @PathVariable repo: String): Flux<Issue>
}
Load-balanced URL:
@CoApi(baseUrl = "lb://order-service")
interface OrderClient
Load-balanced with @LoadBalanced annotation:
Use @LoadBalanced when you want to explicitly mark a client for load balancing regardless of the URL scheme — for example, when the base URL is a plain https:// address that should still go through the load balancer.
@CoApi(baseUrl = "https://order-service")
@LoadBalanced
interface OrderClient {
@GetExchange("/orders/{id}")
fun getOrder(@PathVariable id: String): Order
}
data class Order(val id: String, val status: String)
Requires spring-cloud-starter-loadbalancer on classpath.
Extending a shared API interface: This pattern is useful in microservice architectures where the API contract is defined in a shared module — the provider and consumer both depend on the same interface.
@CoApi(serviceId = "provider-service")
interface TodoClient : TodoApi
No base URL (resolved from config):
When no baseUrl or serviceId is set, the base URL must come from coapi.clients.<name>.baseUrl in application config. This is useful when the target URL varies per environment and should not be hardcoded in the annotation.
@CoApi
interface ConfigResolvedClient {
@GetExchange("/users")
fun getUsers(): Flux<User>
}
Base URL resolution priority (first match wins):
@CoApi(baseUrl = "...") — supports ${property} placeholders resolved at startup@CoApi(serviceId = "...") — auto-constructs lb://serviceIdcoapi.clients.<name>.baseUrl in application config — runtime override per clientDo not set both baseUrl and serviceId on the same @CoApi — the behavior is undefined.
Adding the starter dependency triggers auto-configuration of:
@CoApi-annotated interfacesWebClient or RestClient bean creation per clientHttpServiceProxyFactorySet via coapi.mode in application.yml:
coapi:
mode: AUTO # AUTO | REACTIVE | SYNC
AUTO (default): detects reactive Web stack on classpath → uses WebClient; otherwise uses RestClient. The detection works by checking for org.springframework.web.reactive.HandlerResult, which is present whenever spring-webflux is on the classpath.REACTIVE: forces WebClient-based adaptersSYNC: forces RestClient-based adaptersBy default, scans the Spring Boot application's base package. Override with:
coapi:
base-packages:
- com.example.clients
- com.other.api
Override individual client settings in application.yml:
coapi:
clients:
GitHubApiClient:
base-url: https://api.github.com
load-balanced: false
reactive:
filter:
names:
- loadBalancerExchangeFilterFunction
sync:
interceptor:
names:
- loadBalancerInterceptor
If auto-scanning doesn't fit (e.g., client interfaces live in an external library JAR), use @EnableCoApi to list clients explicitly:
@EnableCoApi(
clients = [
GitHubApiClient::class,
ServiceApiClient::class,
TodoClient::class
]
)
@SpringBootApplication
class MyApp
Requires spring-cloud-starter-loadbalancer on classpath:
implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")
Then configure filters/interceptors per client mode:
Reactive (WebClient): Set filter by bean name or type in coapi.clients.<name>.reactive.filter
Sync (RestClient): Set interceptor by bean name or type in coapi.clients.<name>.sync.interceptor
Tests use me.ahoo.test:fluent-assert-core for assertions — use the .assert() extension, not AssertJ's assertThat(). This provides null-safe, Kotlin-idiomatic assertions.
import me.ahoo.test.asserts.assert
Test annotation parsing with MockEnvironment:
class CoApiDefinitionTest {
@Test
fun toCoApiDefinitionIfServiceApi() {
val coApiDefinition = MockServiceApi::class.java.toCoApiDefinition(MockEnvironment())
coApiDefinition.loadBalanced.assert().isTrue()
coApiDefinition.baseUrl.assert().isEqualTo("http://order-service")
}
}
@CoApi(serviceId = "order-service")
interface MockServiceApi
Use ApplicationContextRunner to test the full bean registration:
class CoApiContextTest {
@Test
fun `should create Reactive CoApi bean`() {
ApplicationContextRunner()
.withPropertyValues("github.url=https://api.github.com")
.withBean(WebClientBuilderCustomizer::class.java, { WebClientBuilderCustomizer.NoOp })
.withUserConfiguration(WebClientAutoConfiguration::class.java)
.withUserConfiguration(EnableCoApiConfiguration::class.java)
.run { context ->
context.assert()
.hasSingleBean(ReactiveHttpExchangeAdapterFactory::class.java)
.hasSingleBean(GitHubApiClient::class.java)
}
}
}
@EnableCoApi(clients = [GitHubApiClient::class])
class EnableCoApiConfiguration
@SpringBootTest
class ConsumerServerTest {
@Autowired
private lateinit var gitHubApiClient: GitHubApiClient
@Test
fun getIssueByGitHubApiClient() {
gitHubApiClient.getIssue("Ahoo-Wang", "Wow")
.doOnNext { println(it) }
.blockLast()
}
}
Use MockK for mocking client interfaces:
@Test
fun `should mock CoApi client`() {
val mockClient = mockk<GitHubApiClient>()
every { mockClient.getIssue("owner", "repo") } returns Flux.just(Issue("url"))
// ... test with mockClient
}
A single interface can mix sync and reactive return types. CoApi's HttpServiceProxyFactory detects the return type per method and uses the appropriate adapter — you don't need separate interfaces for sync and reactive.
@CoApi(baseUrl = "\${github.url}")
interface GitHubSyncClient {
@GetExchange("repos/{owner}/{repo}/issues")
fun getIssue(@PathVariable owner: String, @PathVariable repo: String): List<Issue> // sync
@GetExchange("repos/{owner}/{repo}/issues")
fun getIssueWithReactive(@PathVariable owner: String, @PathVariable repo: String): Flux<Issue> // reactive
}
Pass UriBuilderFactory or URI directly for dynamic URL resolution — useful when the target host is determined at runtime:
@CoApi
interface UriApiClient {
@GetExchange
fun getIssueByUri(uri: URI): Flux<Issue>
@GetExchange
fun getIssue(
uriBuilderFactory: UriBuilderFactory,
@PathVariable owner: String,
@PathVariable repo: String
): Flux<Issue>
}
This typically manifests as NoSuchBeanDefinitionException at startup.
@CoApi is on an interface, not a class — CoApi uses Java dynamic proxies, which only work with interfaces@EnableCoApi)coapi.base-packages if using custom packagesRequests go to a single instance or fail with connection refused.
spring-cloud-starter-loadbalancer is on classpathserviceId is set (or baseUrl uses lb:// prefix), or @LoadBalanced is on the interfacecoapi.clients.<name>The application uses RestClient when you expected WebClient, or vice versa.
coapi.mode propertyAUTO mode: verify if org.springframework.web.reactive.HandlerResult is on classpath — it's present when spring-webflux is a dependencyREACTIVE or SYNC if auto-detection is wrongRequests fail with "no base URL configured" or go to the wrong host.
${...}) require the property to exist in application.yml — a missing property causes startup failureserviceId auto-constructs lb://serviceId — don't also set baseUrl, the two are mutually exclusivecoapi.clients.<name>.baseUrl overrides annotation valuesbaseUrl or serviceId is set on @CoApi, the base URL must come from config@CoApi is meta-annotated with @Component, so interfaces are Spring beans and can be @Autowired directly{name}.HttpClient for the HTTP client, {name}.CoApi for the proxyCoApiFactoryBean creates proxies via Spring's HttpServiceProxyFactoryClientMode.inferClientMode() checks for reactive class on classpath when mode is AUTOAutoCoApiRegistrar (boot) or EnableCoApiRegistrar handles classpath scanning and bean registrationProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub ahoo-wang/skills --plugin ahoo-simba-skills