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:

sequenceDiagram participant Caller participant Proxy as REST Client Proxy participant FT as Fault Tolerance Interceptor participant Remote as Remote Service Caller->>Proxy: findById(id) Proxy->>FT: invoke (Retry + CircuitBreaker) FT->>Remote: HTTP GET /orders/id (attempt 1) Remote-->>FT: 503 Service Unavailable Note over FT: Circuit breaker records a failure,\nfailure ratio still below threshold FT->>FT: Retry policy: wait delay, attempt 2 FT->>Remote: HTTP GET /orders/id (attempt 2) Remote-->>FT: 200 OK (Order payload) Note over FT: Circuit breaker records a success FT-->>Proxy: Order Proxy-->>Caller: Order