This page shows how to declare a typed client interface, inject it through CDI, and invoke a remote HTTP service. No dynamic proxy, no hot reflection: the invocation class is generated at compile time by the cyrano-processor annotation processor (<Interface>$$CyranoClient); the Class-File API only serves as a runtime fallback for interfaces compiled without the processor.

Prerequisites

  • Java 25 — Temurin recommended. Module path enabled.

  • Maven 3.9.16 — pinned via .sdkmanrc at the repo root (sdk env).

  • strict Java Modules — one module-info.java per Maven module.

  • A JSON-B implementation on the classpath/module-path at runtime — Champollion in the Vidocq ecosystem.

  • For CDI integration: Vauban as the container, Ravel for MP Config.

Maven coordinates

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.vidocq.cyrano</groupId>
      <artifactId>cyrano-parent</artifactId>
      <version>0.4.0-SNAPSHOT</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <!-- Public API: RestClientBuilder, MP Rest Client annotations, Cyrano SPI -->
  <dependency>
    <groupId>io.vidocq.cyrano</groupId>
    <artifactId>cyrano-api</artifactId>
  </dependency>

  <!-- Runtime implementation: proxy resolution chain, HttpClient transport,
       Class-File API generator as runtime fallback -->
  <dependency>
    <groupId>io.vidocq.cyrano</groupId>
    <artifactId>cyrano-core</artifactId>
    <scope>runtime</scope>
  </dependency>

  <!-- Optional CDI integration: BCE @RegisterRestClient + @RestClient synthetic bean -->
  <dependency>
    <groupId>io.vidocq.cyrano</groupId>
    <artifactId>cyrano-cdi-vauban</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

<!-- Compile-time client generation (APT-first): generates <Interface>$$CyranoClient
     sources for every @RegisterRestClient interface -->
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>io.vidocq.cyrano</groupId>
            <artifactId>cyrano-processor</artifactId>
            <version>0.4.0-SNAPSHOT</version>
          </path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>
cyrano-processor registers itself through META-INF/services/javax.annotation.processing.Processor, so declaring it as a provided-scope dependency also triggers it (javac ServiceLoader discovery). It is optional: without it, cyrano-core generates the proxy at runtime through the Class-File API — the compile-time path is simply faster to boot and AOT-safe (GraalVM native-image, Leyden CDS).
cyrano-mp-rest-client-api is pulled in transitively. This module repackages the MicroProfile Rest Client spec (which ships with no module-info and no Automatic-Module-Name upstream) to produce an explicit Java module io.vidocq.cyrano.mp.rest.client.api. Without it, jlink fails.

module-info.java

module com.example.client {
    requires io.vidocq.cyrano.api;
    requires io.vidocq.cyrano.mp.rest.client.api;
    requires jakarta.ws.rs;
    requires jakarta.cdi;

    // So JSON-B can deserialise returned DTOs
    opens com.example.client.dto to jakarta.json.bind;
}

Step 1 — Describe the remote service as an interface

package com.example.client;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterRestClient(configKey = "users-api")
@Path("/users")
public interface UsersClient {

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    UserDto findById(@PathParam("id") long id);
}

@RegisterRestClient(configKey = "users-api") registers the interface with the Cyrano BCE and provides the MP Config key that drives the base URL.

Step 2 — Configure the base URL

In META-INF/microprofile-config.properties (resolved by Ravel):

users-api/mp-rest/url=https://api.example.com
users-api/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped
users-api/mp-rest/connectTimeout=2000
users-api/mp-rest/readTimeout=5000

Resolution follows MP Rest Client priority: <configKey>/mp-rest/uri > <configKey>/mp-rest/url > @RegisterRestClient(baseUri=…​).

Step 3 — Inject and call

package com.example.app;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import com.example.client.UsersClient;
import com.example.client.UserDto;

@ApplicationScoped
public class UserService {

    @Inject
    @RestClient
    UsersClient users;

    public String greet(long id) {
        UserDto user = users.findById(id);
        return "Hello " + user.name();
    }
}

At bootstrap, the CyranoRestClientCdiExtension BCE detects UsersClient via @Enhancement(types = Object.class, withAnnotations = RegisterRestClient.class, withSubtypes = true), synthesises a @RestClient-qualified bean (@Synthesis), and delegates instantiation to RestClientBuilder.newBuilder().baseUri(…​).build(UsersClient.class). The builder resolves the proxy through ClientProxyRegistry: the APT-generated UsersClient$$CyranoClient (compiled with the client interface) is picked up first; only when no generated client exists does CyranoProxyGenerator emit a Cyrano$UsersClient class at runtime via the Class-File API.

Variant without CDI — RestClientBuilder

If the application runs in pure SE (no CDI), build the client by hand:

import java.net.URI;
import org.eclipse.microprofile.rest.client.RestClientBuilder;

UsersClient users = RestClientBuilder.newBuilder()
    .baseUri(URI.create("https://api.example.com"))
    .build(UsersClient.class);

UserDto user = users.findById(42L);

cyrano-core is sufficient for this mode — cyrano-cdi-vauban is not required.

Step 4 — Run

cd cyrano
sdk env
./mvnw -ntp install -DskipTests
./mvnw test

To run the official MicroProfile Rest Client 4.0 TCK, see the TCK page (./run-official-tck-mp-rest-client-4.0.sh).

What next?

  • Concepts — MP Rest Client vocabulary.

  • Usage — async, dynamic headers, exception mappers, providers.

  • Reference — every supported MP Config key.