Interacting with REST APIs in Quarkus

In the last post, we used Spring compatible APIs to rebuild our original REST APIs in a Quarkus application. In this post, we will interact with the REST APIs in the client side.

There a few HTTP Client libraries used to communicate with REST APIs, such as Apache HTTPClient, OkHttp, etc. And Spring has specific RestTemplate, WebClient API can be used to interact with REST APIs. And Java 11 also ship a new stable HttpClient API.

Quarkus has built-in support of the latest Microprofile, which includes a Rest Client spec for this purpose, the Quarkus rest-client supports both MP RestClient and JAX-RS Client API. In this posts, we will explore MP RestClient, JAX-RS Client and Java 11 HttpClient one by one.

MicroProfile Rest Client

First of all, let’s have a look at how to use MP RestClient to consume REST APIs.

Generate a simple Quarkus application using Quarkus Coding, remember adding Rest Client into dependencies.

Create a simple interface PostResourceClient.

@Path("/posts")
@RegisterRestClient
public interface PostResourceClient {
@Path("count")
@GET
@Produces(MediaType.APPLICATION_JSON)
CompletionStage<Long> countAllPosts(@QueryParam("q") String q);
@GET
@Produces(MediaType.APPLICATION_JSON)
CompletionStage<List<Post>> getAllPosts(
@QueryParam("q") String q,
@QueryParam("offset") @DefaultValue("0") int offset,
@QueryParam("limit") @DefaultValue("10") int limit
);
}

The class and method signature is similar with PostResource we have created in the former posts.

  • We declare an interface instead of a class.
  • The interface is annotated with the @RegisterRestClient annotation.
  • The method can return entity type directly or a Jaxrs’s Response or Java 8's CompletionStage wrapper.

You can configure the baseUri in the application.properties, or set baseUri attribute the @RegisterRestClient annotation.

Here is an example of the configuration in the application.properties.

com.example.PostResourceClient/mp-rest/url=http://localhost:8080
com.example.PostResourceClient/mp-rest/scope=javax.inject.Singleton

By default it uses the full qualified name of the Rest Client class as prefix of the config key. To change this, just set configKey attribute in the @RegisterRestClient annotation.

To use the PostResourceClient in your CDI bean, just inject it by @Inject with a CDI qualifier @RestClient.

@Path("/api")
@RequestScoped
public class PostController {
@Inject
@RestClient
PostResourceClient client;
@GET
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<PostPage> getAllPosts(
@QueryParam("q") String q,
@QueryParam("offset") @DefaultValue("0") int offset,
@QueryParam("limit") @DefaultValue("10") int limit
) {
return this.client.getAllPosts(q, offset, limit)
.thenCombine(
this.client.countAllPosts(q),
(data, count) -> PostPage.of(data, count)
);
}
}

The above getAllPosts method is trying to combine the data and count into a new PostPage instance.

To test if the client is working as expected. Firstly you should start the server side application to serve the REST APIs. Run it by mvn quarkus:dev command.

Then run the client application we are building by mvn quarkus:dev again.

$ curl http://localhost:8081/api/
{"content":[{"content":"My second post of Quarkus","createdAt":"2020-04-02T17:02:06.453988","id":"768a980f-b617-40c1-ba07-60759045d6b7","title":"Hello Again, Quarkus"},{"content":"My first post of Quarkus","createdAt":"2020-04-02T17:02:06.452993","id":"675bf657-fce4-4724-96c3-a2e2bf38ad58","title":"Hello Quarkus"}],"count":2}

In our PostResource , to access a none existing post resource, it will return a 404 HTTP status. Add the following method signature in the PostResouceClient.

public interface PostResourceClient {
...
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
CompletionStage<Post> getPostById(@PathParam("id") String id);
}

And create a new method in the PostController to invoke the above APIs.

public class PostController {
...
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<Post> getPost(@PathParam("id") String id ){
return this.client.getPostById(id);
}
}

When accessing a none existing post in the client application, it also return a 404 HTTP status.

If you do not want MP Rest Client to convert it automatically, you can use ResponseExceptionMapper to convert the failure status into a custom exception, and that leave room for you to handle it as you expected.

public class PostResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
@Override
public RuntimeException toThrowable(Response response) {
if(response.getStatus()==404) {
return new PostNotFoundException("post not found, original cause:" + response.readEntity(String.class));
}
return null;
}
}

And register it on the PostResourceClient class with a @RegisterProvider annotation.

@RegisterProvider(PostResponseExceptionMapper.class)
public interface PostResourceClient {}

Now, trying to accessing a none existing post resource, it will throw an PostNotFoundException in the background.

As an example, you can handle this exception with a custom ExceptionMapper in our client application. Check the PostNotFoundExceptionMapper yourself.

JAX-RS Client API

Besides the declarative means provided in MP Rest Client, JAX-RS 2.0 provides a Client API to shake hands with the REST APIs in a programmatic way.

Let’s have a look at the JAX-RS version of PostResourceClient.

@ApplicationScoped
public class PostResourceClient {
private ExecutorService executorService = Executors.newCachedThreadPool();
private Client client;
private String baseUrl ;//= "http://localhost:8080";
@Inject
public PostResourceClient(PostServiceProperties properties) {
baseUrl = properties.getBaseUrl();
client = ClientBuilder.newBuilder()
.executorService(executorService)
.build();
}
CompletionStage<Long> countAllPosts(String q) {
return client.target(baseUrl + "/posts/count")
.queryParam("q", q)
.request()
.rx()
.get(Long.class);
}
CompletionStage<List<Post>> getAllPosts(
String q,
int offset,
int limit
) {
return client.target(baseUrl + "/posts")
.queryParam("q", q)
.queryParam("offset", offset)
.queryParam("limit", limit)
.request()
.rx()
.get(new GenericType<List<Post>>() {});
}
}

It uses the JAX-RS’s ClientBuilder to build a Client firstly, then use it to call REST APIs.

The PostServiceProperties is a MP config class to read external configuration from the application.properties file.

@ConfigProperties(prefix = "post-service")
public class PostServiceProperties {
private String baseUrl;
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}

And the content of the application.properties file.

post-service.base-url=http://localhost:8080

But if you are using the JAX-RS client API, you should handle exceptions manually. For example, calling the REST API like this in the PostResourceClient.

Post getPostById(String id) {
try (Response getPostByIdResponse = client.target(baseUrl + "/posts/" + id)
.request().get()) {
if (getPostByIdResponse.getStatus() == 404) {
throw new PostNotFoundException(id);
}
return getPostByIdResponse.readEntity(Post.class);
}
}

Next, let’s look at the HttpClient in Java 11.

Java 11 HttpClient

If you are using Java 11, it is luckily to use the brand new HttpClient API which is stabilized to public since Java 11.

The following is a new version using Java 11 HttpClient.

@ApplicationScoped
public class PostResourceClient {
private final ExecutorService executorService = Executors.newFixedThreadPool(5); private final HttpClient httpClient = HttpClient.newBuilder()
.executor(executorService)
.version(HttpClient.Version.HTTP_2)
.build();
public PostResourceClient() {
}
CompletionStage<Long> countAllPosts(String q) {
return this.httpClient
.sendAsync(
HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://localhost:8080/posts/count?q=" + q))
.header("Accept", "application/json")
.build()
,
HttpResponse.BodyHandlers.ofString()
)
.thenApply(HttpResponse::body)
.thenApply(Long::parseLong)
.toCompletableFuture();
} CompletionStage<List<Post>> getAllPosts(
String q,
int offset,
int limit
) {
return this.httpClient
.sendAsync(
HttpRequest.newBuilder()
.GET()
.uri(URI.create("http://localhost:8080/posts?q=" + q + "&offset=" + offset + "&limit=" + limit))
.header("Accept", "application/json")
.build()
,
HttpResponse.BodyHandlers.ofString()
)
.thenApply(HttpResponse::body)
.thenApply(stringHttpResponse -> JsonbBuilder.newBuilder().build().fromJson(stringHttpResponse, new TypeLiteral<List<Post>>() {}.getType()))
.thenApply(data ->(List<Post>)data)
.toCompletableFuture();
}
}

In the getAllPosts method , it uses JSON-B to convert the raw HTTP messages from String to a List<Post>.

Get the source codes from my Github.

Written by

Self-employed technical consultant, solution architect and full-stack developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store