DEV Community

Cover image for Building Robust REST Client with Quarkus: A Comprehensive Guide
Ivelin Yanev
Ivelin Yanev

Posted on

Building Robust REST Client with Quarkus: A Comprehensive Guide

Introduction

When building modern microservices applications, consuming external REST services is a common requirement. Quarkus provides powerful built-in support for REST clients, offering both traditional Java EE approaches and modern MicroProfile solutions. In this article, we'll focus on the MicroProfile REST Client approach, as it provides a more modern, maintainable, and feature-rich way to build REST clients in Quarkus applications. This approach aligns better with microservices architectures and provides superior integration with other MicroProfile features like Config, Fault Tolerance, and Metrics.

It offers:

  • Declarative, interface-based client definitions;
  • Type-safe request/response handling;
  • Automatic client generation;
  • Built-in integration with MicroProfile Configl
  • Better integration with CDI and other MicroProfile features;
  • Simplified error handling and resilience pattern.

Rest Client

Getting Started

  1. Adding Required Dependencies

    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-jackson</artifactId>
    </dependency>
    
  2. Creating Your First REST Client

        @RegisterRestClient(configKey = "simple-api")
         public interface SimpleRestClient {
         @GET
         @Path("/simple")
         Response simple();
      }
    
  3. Configuration

Configure your REST client in application.properties:

quarkus.rest-client.simple-api.url=http://simple-api:8080
Enter fullscreen mode Exit fullscreen mode
  • @RegisterRestClient: Registers the interface for CDI injection as a REST Client;
  • @Path, @GET: Standard Jakarta REST annotations defining service access;
  • configKey: Allows using a custom configuration key instead of the fully qualified interface name.

Real-World Example: Cryptocurrency API Client

Let's build a practical example using the CoinCap API to fetch cryptocurrency data.

@RegisterRestClient(configKey = "crypto-currency")
public interface CryptoCurrencyRestClient {
    @GET
    @Path("/assets")
    AssetData getAssets();

    @GET
    @Path("/assets/{id}/history")
    AssetHistoryData getAssetHistory(
        @PathParam("id") String id,
        @QueryParam("interval") String interval
    );
}
Enter fullscreen mode Exit fullscreen mode

and the record classes:

public record AssetData(List<Crypto> data) {
  public record Crypto(String id,
                       String rank,
                       String symbol,
                       String name,
                       String supply,
                       String maxSupply,
                       String marketCapUsd,
                       String volumeUsd24Hr,
                       String priceUsd,
                       String changePercent24Hr,
                       String vwap24Hr,
                       String explorer) {

  }
}

public record AssetHistoryData(List<DataPoint> data) {
  public record DataPoint(
      String priceUsd,
      long time,
      String circulatingSupply,
      String date
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

You can easily inject and use the REST client within your Quarkus application. Implementation in a Resource:

@Path("/crypto")
public class CryptoCurrencyResource {
    private final CryptoCurrencyRestClient cryptoCurrencyRestClient;

    public CryptoCurrencyResource(@RestClient CryptoCurrencyRestClient cryptoCurrencyRestClient) {
        this.cryptoCurrencyRestClient = cryptoCurrencyRestClient;
    }

    @GET
    public AssetData getCurrency() {
        return cryptoCurrencyRestClient.getAssets();
    }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Fault Tolerance

In distributed systems, network calls can fail. Quarkus provides robust fault tolerance features through SmallRye Fault Tolerance:

  1. Add the dependency:

    <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-fault-tolerance</artifactId>
    </dependency>
    
  2. Implement fault tolerance patterns:

Retry Pattern

The @Retry annotation provides sophisticated retry capabilities when service calls fail. Here are the key configuration options:

@GET
@Path("/assets/{id}/history")
@Retry(
    maxRetries = 5,                    // Maximum number of retry attempts
    delay = 200,                       // Delay between retries in milliseconds
    jitter = 100,                      // Random variation added to delay
    retryOn = {                        // Specify which exceptions trigger retry
        IOException.class,
        TimeoutException.class
    },
    abortOn = {                        // Exceptions that immediately abort retry
        SecurityException.class,
        IllegalArgumentException.class
    },
    maxDuration = 2000                 // Maximum duration for all retry attempts
)
AssetHistoryData getAssetHistory(
    @PathParam("id") String id,
    @QueryParam("interval") String interval
);
Enter fullscreen mode Exit fullscreen mode

You can also implement exponential backoff:

@Retry(
    maxRetries = 3,
    delay = 1000,
    delayUnit = ChronoUnit.MILLIS,
    exponential = true                 // Enable exponential backoff
)
Enter fullscreen mode Exit fullscreen mode

Fallback Pattern

Fallbacks provide alternative behavior when all retry attempts fail. You can implement fallbacks in multiple ways:

@GET
@Path("/assets")
@Retry(maxRetries = 2)
@Fallback(fallbackMethod = "getDefaultAssets")
AssetData getAssets();

// Fallback method must have the same return type and parameters
private AssetData getDefaultAssets() {
    log.warn("Using fallback method for getAssets");
    return AssetData.builder()
        .timestamp(System.currentTimeMillis())
        .status("FALLBACK")
        .build();
}
Enter fullscreen mode Exit fullscreen mode

Circuit Breaker Pattern

The Circuit Breaker pattern prevents cascade failures in distributed systems. Here's a detailed configuration:

@GET
@Path("/assets")
@CircuitBreaker(
    requestVolumeThreshold = 4,       // Minimum requests before CB can trip
    failureRatio = 0.5,               // Failure ratio to trip CB (50%)
    delay = 1000,                     // Time CB stays open before half-open
    successThreshold = 2,             // Successes needed to close CB
    failOn = {                        // Exceptions counted as failures
        IOException.class,
        TimeoutException.class
    },
    skipOn = {                        // Exceptions not counted as failures
        BusinessException.class
    }
)
@Fallback(fallbackMethod = "getDefaultAssets")  // Combine with fallback
AssetData getAssets();
Enter fullscreen mode Exit fullscreen mode

Circuit Breaker States:

  • CLOSED: Normal operation, calls pass through
  • OPEN: Calls fail fast without executing
  • HALF-OPEN: Limited calls allowed to test service recovery

Dynamic Base URL Configuration

The @Url annotation provides flexible runtime URL configuration:

  1. Basic Usage:

     @GET
     @Path("/assets")
     AssetData getAssets(@Url String url);
     // Usage in code
     client.getAssets("https://alternate-api.example.com/v2");
    
  2. Combining with Path Parameters:

     @GET
     @Path("/assets/{id}")
     AssetData getAssetById(@Url String baseUrl, @PathParam("id") String id);
     // Usage
     client.getAssetById("https://api-backup.example.com", "bitcoin");
    
  3. URL Priority:

     // Configuration priority (highest to lowest):
     // 1. @Url parameter
     // 2. Programmatic configuration
     // 3. application.properties
     // 4. Default URL (if specified in @RegisterRestClient)
    
     @RegisterRestClient(baseUri = "https://default.api.example.com")
     public interface DynamicRestClient {
        @GET
        @Path("/resource")
         Response getResource(@Url String url);
       }
    

Custom Headers Management

Header management is crucial for authentication, tracking, and protocol compliance:

  1. Static Headers

    @RegisterRestClient(configKey = "api-client")
    @ClientHeaderParam(name = "API-Version", value = "1.0")
    @ClientHeaderParam(name = "Client-ID", value = "${app.client.id}")
    public interface ApiClient {
       @GET
       @Path("/data")
       Response getData();
    }
    
  2. Dynamic Headers using Methods:

@RegisterRestClient(configKey = "secure-api")
public interface SecureApiClient {
    @GET
    @Path("/protected-resource")
    @ClientHeaderParam(name = "Authorization", value = "{generateAuthHeader}")
    @ClientHeaderParam(name = "Request-Time", value = "{generateTimestamp}")
    Response getProtectedResource();

    default String generateAuthHeader() {
        return "Bearer " + TokenGenerator.generateToken();
    }

    default String generateTimestamp() {
        return String.valueOf(System.currentTimeMillis());
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Example: Integrating with Freesound API

Let's explore a practical example of building a REST client for the Freesound API, which demonstrates header-based authentication and parameter handling.

  1. Creating the REST Client Interface

       @RegisterRestClient(configKey = "free-sound")
       @ClientHeaderParam(name = "Authorization", value ={getAuthorizationHeader}")
       public interface FreeSoundRestClient {
    
          default String getAuthorizationHeader() {
            Config config = ConfigProvider.getConfig();
            String apiKey = config.getConfigValue("free.sound.api.key").getValue();
            return "Token " + apiKey;
        }
    
        @GET
        @Path("/users/{username}")
        UserResponse getUsers(@PathParam("username") String username);    
    }
    
  2. Configuration Setup

     # Base URL configuration
     quarkus.rest-client.free-sound.url=https://freesound.org/apiv2
    
     # API Key configuration
     free.sound.api.key=${FREESOUND_API_KEY}
    
  3. Response Data Models

      public record UserResponse(String url,
                           String username,
                           String about,
                           String home_page,
                           Avatar avatar,
                           LocalDateTime date_joined,
                           int num_sounds,
                           String sounds,
                           int num_packs,
                           String packs,
                           int num_posts,
                           int num_comments) {
    
     public record Avatar(
      String small,
      String medium,
      String large
      ) {}
    }
    
  4. Using the Client in a Resource

    @Path("/freesound")
    @Produces(MediaType.APPLICATION_JSON)
    public class FreeSoundResource {
    private final FreeSoundRestClient freeSoundClient;
    
    public FreeSoundResource(@RestClient FreeSoundRestClient freeSoundClient) {
        this.freeSoundClient = freeSoundClient;
    }
    
    @GET
    @Path("/users/{username}")
    public UserResponse getUserProfile(@PathParam("username") String username) {
        return freeSoundClient.getUsers(username);
    }
    }
    

Conclusion

Quarkus REST Client provides a powerful and flexible way to consume REST services in your microservices architecture. By combining MicroProfile REST Client with Quarkus's fault tolerance features, you can build robust and reliable service communications.
The full source code for this article is available on GitHub

Top comments (0)