The MicroProfile REST Client
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The MicroProfile REST Client lets a project call a remote HTTP endpoint through a plain Java interface annotated with the same JAX-RS annotations used to declare an endpoint, rather than hand-assembling requests with a low-level HTTP client. Quarkus implements the specification and layers its own configuration, reactive support, and fault-tolerance integration on top of it. Where Quarkus REST is the server-side counterpart — exposing JAX-RS resources — this page is about the client side: declaring a typed interface for someone else’s endpoint, injecting or building it, shaping headers and payloads, mapping error responses to typed exceptions, and composing the whole thing with retries, circuit breakers, and service discovery.
Declaring a typed client interface
Extension and dependency
The REST Client ships as its own extension, with a JSON-provider extension added alongside it depending on which JSON library the project wants (Jackson is the common default; JSON-B is the JAX-RS/MicroProfile-standard alternative):
./mvnw quarkus:add-extension -Dextensions=rest-client-jackson
# or, for JSON-B instead of Jackson:
./mvnw quarkus:add-extension -Dextensions=rest-client-jsonb
Both extensions pull in quarkus-rest-client (or, for the reactive/Mutiny-first variant used further down this
page, quarkus-rest-client-reactive and its own -jackson/-jsonb companion) transitively — there is no need
to add the base artifact separately.
The interface
A REST client is a plain interface: @Path, @GET/@POST/etc., @Produces/@Consumes, and parameter
annotations are the exact same JAX-RS annotations a server-side resource would use, plus one Quarkus/
MicroProfile-specific annotation, @RegisterRestClient, that turns the interface into an injectable client and
gives it a configKey used to look up its configuration:
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 = "countries-api")
@Path("/v3.1")
public interface CountriesClient {
@GET
@Path("/name/{name}")
@Produces(MediaType.APPLICATION_JSON)
List<Country> byName(@PathParam("name") String name);
}
(Country is an ordinary record or POJO matching the JSON shape returned by the remote service — the same kind
of type a JAX-RS resource method would return.)
Configuration
The current Quarkus-recommended style keys configuration off the configKey under quarkus.rest-client.*:
quarkus.rest-client."countries-api".url=https://restcountries.com/v3.1
quarkus.rest-client."countries-api".scope=jakarta.inject.Singleton
The portable, spec-level equivalent — understood by any MicroProfile REST Client implementation, not just
Quarkus — uses the configKey as a property prefix followed by /mp-rest/:
countries-api/mp-rest/url=https://restcountries.com/v3.1
Both forms are read at the same ordinal precedence described on the configuration
page; new code should default to quarkus.rest-client.*, reaching for mp-rest only when a client
configuration must remain runnable, unchanged, on a non-Quarkus MicroProfile runtime.
Consuming it: CDI injection
The usual way to obtain a client is @RestClient-qualified CDI injection, which resolves the interface’s
implementation by its configKey-backed configuration automatically:
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import com.example.client.CountriesClient;
@ApplicationScoped
public class CountryLookupService {
@RestClient
CountriesClient countriesClient;
public List<Country> lookup(String name) {
return countriesClient.byName(name);
}
}
Consuming it: RestClientBuilder
When the target URL is only known at runtime — resolved from a database row, a multi-tenant configuration, or
computed rather than statically configured — the same interface can be built programmatically instead, without
any @RegisterRestClient/configKey machinery at all:
package com.example;
import java.net.URI;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import com.example.client.CountriesClient;
public class DynamicClientFactory {
public CountriesClient forBaseUri(URI baseUri) {
return RestClientBuilder.newBuilder()
.baseUri(baseUri)
.build(CountriesClient.class);
}
}
A programmatically-built client still honors any @RegisterProvider/@ClientHeaderParam annotations declared
on the interface itself — only the URL resolution and CDI injection step is bypassed.
Headers, parameters, and payload shapes
@ClientHeaderParam: static and computed header values
@ClientHeaderParam attaches a header to every call of the annotated method (or every method of the interface,
if placed at the type level). A literal value is fixed at compile time; prefixing the value with a method
reference computes it per call:
package com.example.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
@Path("/orders")
public interface OrdersClient {
@GET
@ClientHeaderParam(name = "X-Client-Name", value = "billing-service")
@ClientHeaderParam(name = "X-Request-Id", value = "{generateRequestId}")
List<Order> list();
default String generateRequestId() {
return java.util.UUID.randomUUID().toString();
}
}
ClientHeadersFactory: propagating or transforming headers
Forwarding a header from the incoming server request onto an outgoing client call — the common case being
an Authorization bearer token that should flow through to a downstream service unchanged — is not something
@ClientHeaderParam can express on its own, since it has no access to the current request. A
ClientHeadersFactory does:
package com.example.client;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import org.jboss.resteasy.reactive.client.impl.MultivaluedMapImpl;
import jakarta.ws.rs.core.MultivaluedMap;
@ApplicationScoped
public class AuthorizationPropagatingFactory implements ClientHeadersFactory {
@Override
public MultivaluedMap<String, String> update(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
MultivaluedMap<String, String> result = new MultivaluedMapImpl<>();
String authorization = incomingHeaders.getFirst("Authorization");
if (authorization != null) {
result.add("Authorization", authorization);
}
return result;
}
}
wired onto the client interface with @RegisterClientHeaders:
package com.example.client;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
@RegisterRestClient(configKey = "orders-api")
@RegisterClientHeaders(AuthorizationPropagatingFactory.class)
@Path("/orders")
public interface OrdersClient {
// ...
}
incomingHeaders is only populated when the client call happens inside the scope of an active JAX-RS request — a REST resource method calling another service on the caller’s behalf — and is empty for a client call made
from a scheduled job or a message consumer with no inbound HTTP request of its own.
Query and path parameters
@PathParam, @QueryParam, and @HeaderParam on method parameters work exactly as they do on a server-side
resource method:
@GET
@Path("/name/{name}")
List<Country> byName(@PathParam("name") String name, @QueryParam("fields") String fields);
Multipart requests
A multipart request is expressed as a form-data-annotated parameter type, @Consumes(MediaType.MULTIPART_FORM_DATA),
and a @RestForm-annotated request body:
package com.example.client;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.multipart.FileUpload;
@Path("/uploads")
public interface UploadClient {
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
UploadResult upload(FileUploadForm form);
}
package com.example.client;
import org.jboss.resteasy.reactive.PartType;
import org.jboss.resteasy.reactive.RestForm;
import jakarta.ws.rs.core.MediaType;
import java.io.File;
public class FileUploadForm {
@RestForm
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public File file;
@RestForm
@PartType(MediaType.TEXT_PLAIN)
public String description;
}
Pluggable JSON providers
Which JSON library a client (de)serializes payloads with is a function of which extension is on the classpath,
not a per-call choice: rest-client-jackson wires in com.fasterxml.jackson.databind.ObjectMapper-based
(de)serialization, rest-client-jsonb wires in jakarta.json.bind.Jsonb-based (de)serialization instead. Both
providers honor the same Jackson/JSON-B annotations (@JsonProperty, @JsonbProperty, and so on) already used
elsewhere in the project, and both are configured the same way as the server-side counterpart described on the
Quarkus REST page — there is no separate client-specific JSON
configuration to learn.
Reactive vs. blocking return types
The exact same interface style supports either a blocking return type or a Uni<T>/Multi<T> return type
powered by Mutiny — the choice is made per method, by the declared return
type, not by a separate client flavor:
package com.example.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import io.smallrye.mutiny.Uni;
@Path("/v3.1")
public interface CountriesClient {
// Blocking: the calling thread waits for the response.
@GET
@Path("/name/{name}")
List<Country> byName(@PathParam("name") String name);
// Reactive: returns immediately, completes the Uni once the response arrives.
@GET
@Path("/name/{name}")
Uni<List<Country>> byNameAsync(@PathParam("name") String name);
}
A blocking call made from an I/O thread is automatically dispatched to a worker thread by the same event-loop
rules the server side follows; a Uni/Multi-returning call stays on the I/O thread end to end, which is the
better fit inside an already-reactive request handler chained with other non-blocking work.
Exception mapping, TLS, and connection behavior
ResponseExceptionMapper: typed exceptions instead of manual Response inspection
Without a mapper, a non-2xx response either throws a generic WebApplicationException or, for a method returning
a plain domain type, does not deserialize cleanly at all — forcing callers to inspect a raw Response for every
call just to detect failure. ResponseExceptionMapper<T> moves that translation to one place:
package com.example.client;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper;
public class OrderNotFoundExceptionMapper implements ResponseExceptionMapper<OrderNotFoundException> {
@Override
public OrderNotFoundException toThrowable(Response response) {
if (response.getStatus() == 404) {
return new OrderNotFoundException();
}
return null; // let another mapper, or the default handling, take over
}
}
registered on the interface with @RegisterProvider:
@RegisterRestClient(configKey = "orders-api")
@RegisterProvider(OrderNotFoundExceptionMapper.class)
@Path("/orders")
public interface OrdersClient {
// ...
}
Callers can now catch OrderNotFoundException directly instead of branching on a status code returned from
every call site.
TLS/SSL configuration and the centralized TLS registry
The modern way to configure trust and key material for a REST client is the centralized TLS registry
(quarkus.tls.*), which defines a named TLS configuration once and shares it across REST clients, datasources,
and any other extension that needs TLS, rather than repeating trust-store settings per client:
quarkus.tls.orders-api-tls.trust-store.pem.certs=certs/orders-api-ca.pem
quarkus.rest-client."orders-api".tls-configuration-name=orders-api-tls
The older, client-scoped keys remain available for a single client with no shared TLS configuration to name:
quarkus.rest-client."orders-api".trust-store=classpath:/certs/orders-api-truststore.p12
quarkus.rest-client."orders-api".trust-store-password=changeit
quarkus.rest-client."orders-api".key-store=classpath:/certs/orders-api-keystore.p12
quarkus.rest-client."orders-api".key-store-password=changeit
Timeouts, redirects, and connection pooling
quarkus.rest-client."orders-api".connect-timeout=3000
quarkus.rest-client."orders-api".read-timeout=10000
quarkus.rest-client."orders-api".follow-redirects=true
# connection pooling
quarkus.rest-client."orders-api".connection-pool-size=20
quarkus.rest-client."orders-api".connection-ttl=30000
connect-timeout/read-timeout are in milliseconds; a read-timeout that trips surfaces to the caller as the
same kind of failure a network-level timeout would, which is exactly what makes it worth composing with
@Retry/@CircuitBreaker rather than treated as a one-off error, covered next.
Composing with fault tolerance, Stork, and logging
Stacking @Retry and @CircuitBreaker
Because a REST client is CDI-managed, SmallRye Fault Tolerance annotations apply directly to its interface methods, exactly as they would on any other CDI bean method — no adapter code needed:
package com.example.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import java.time.temporal.ChronoUnit;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Retry;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "orders-api")
@Path("/orders")
public interface OrdersClient {
@GET
@Path("/{id}")
@Retry(maxRetries = 3, delay = 200, delayUnit = ChronoUnit.MILLIS)
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
Order findById(@PathParam("id") String id);
}
The full semantics of each annotation — back-off, jitter, fallback methods, bulkheads, timeout budget across a retried call — belong to the fault tolerance and resilience page; the point here is only that they attach to a REST client interface exactly as they would anywhere else.
Stork: client-side service discovery instead of a hardcoded URL
Rather than pointing a client at one fixed URL, Stork resolves the target host per call from a pluggable
service-discovery mechanism — static list, Consul, Kubernetes, and others — and load-balances across the
resolved instances. The client configuration points at a Stork service name with the stork:// scheme instead
of a real host:
quarkus.rest-client."orders-api".url=stork://orders-service/orders
quarkus.stork.orders-service.service-discovery.type=static
quarkus.stork.orders-service.service-discovery.address-list=orders-1:8080,orders-2:8080
The OrdersClient interface itself needs no change at all — Stork resolution is entirely a configuration-level
concern. Deeper coverage of service-discovery types and load-balancing strategies belongs to the
fault tolerance and resilience page.
Logging outgoing requests and responses
# every client, globally
quarkus.rest-client.logging.scope=request-response
quarkus.rest-client.logging.body-limit=10000
# one client only, overriding the global scope
quarkus.rest-client."orders-api".logging.scope=all
scope accepts none (default), request-response (method, URL, status, headers), or all (adds request/
response bodies, truncated at body-limit bytes) — useful while diagnosing an integration, but body logging
should stay off in production for any client carrying sensitive payloads.
Testing REST clients
Point a client’s url at a WireMock server or a Quarkus Dev Service-backed stub started for the test, rather
than the real remote service, and record/verify the exact requests the client interface produces the same way
any other HTTP interaction would be tested; this is covered in depth, including @QuarkusTestResource wiring, on
the testing page.
Retry and circuit breaker in sequence
The diagram below traces one client call that fails once, is retried by @Retry, succeeds on the second
attempt, and is observed throughout by the @CircuitBreaker interceptor tracking its rolling failure ratio: