Consuming GraphQL APIs with Vertx HttpClient/WebClient

Hantsy
ITNEXT
Published in
4 min readJul 24, 2021

--

In a previous post, we have covered consuming RESTful APIs with Vertx HttpClient and WebClient. In this post, we will consuming the GraphQL APIs we created in the last post.

Photo by Sam Balye on Unsplash

Firstly, let’s review the difference between the HttpClient and WebClient.

  • The HttpClient is a low level API, which provides fine-grained control to interact with the Http Server.
  • The WebClient is a high level API, which provides more convenient approaches to simplify handling web request and response. In most of the cases, WebClient is preferred, but it lacks of WebSocket support, so when starting a WebSocket connection, we have to switch to HttpClient.

Checkout the complete sample codes from my Github.

Assume you have read the GraphQL over HTTP specification and GraphQL multipart request specification.

Create a Eclipse Vertx project through the Eclipse Vertx Starter.

In the start method of the MainVerticle class, create a WebClient object firstly.

var options = new WebClientOptions()
.setUserAgent(WebClientOptions.loadUserAgent())
.setDefaultHost("localhost")
.setDefaultPort(8080);
var client = WebClient.create(vertx, options);

The following is an example sending a GraphQL request to retrieve all posts.

client.post("/graphql")
.sendJson(Map.of(
"query", "query posts{ allPosts{ id title content author{ name } comments{ content createdAt} createdAt}}",
"variables", Map.of()
))
.onSuccess(
data -> log.info("data of allPosts: {}", data.bodyAsString())
)
.onFailure(e -> log.error("error: {}", e));

The request body format is like the following.

{
"query": "...",
"operationName": "...",
"variables": { "myVariable": "someValue", ... }
}

The query is accepting a Schema Definition Language in string. The operationName and variables are optional.

And the response body is like.

{
"data": "...",
"errors": "..."
}

When an error occurs, data is empty, and errors will contains the error details to describe the error or exception.

Unlike RESTful APIs, in most of the cases, if there is an error occurred, and the error is from our application itself, the HTTP response status code is always 200.

The following example is to demonstrate how to create a post and then retrieve the newly created post by id.

client.post("/graphql")
.sendJson(Map.of(
"query", "mutation newPost($input:CreatePostInput!){ createPost(createPostInput:$input)}",
"variables", Map.of(
"input", Map.of("title", "create post from WebClient", "content", "content of the new post")
)
))
.onSuccess(
data -> {
log.info("data of createPost: {}", data.bodyAsString());
var createdId = data.bodyAsJsonObject().getJsonObject("data").getString("createPost");
// get the created post.
getPostById(client, createdId);
// add comment.
addComment(client, createdId);
}
)
.onFailure(e -> log.error("error: {}", e));
//getPostById
private void getPostById(WebClient client, String id) {
client.post("/graphql")
.sendJson(Map.of(
"query", "query post($id:String!){ postById(postId:$id){ id title content author{ name } comments{ content createdAt} createdAt}}",
"variables", Map.of(
"id", id
)
))
.onSuccess(
data -> log.info("data of postByID: {}", data.bodyAsString())
)
.onFailure(e -> log.error("error: {}", e));
}

Add a addComment method to demonstrate the progress of creating a comment for the post, at the same it subscribes the commentAdded event (Subscription).

private void addComment(WebClient client, String id) {
// switch to HttpClient to handle WebSocket
var options = new HttpClientOptions()
.setWebSocketClosingTimeout(7200)
.setDefaultHost("localhost")
.setDefaultPort(8080);
// see: https://github.com/vert-x3/vertx-web/blob/master/vertx-web-graphql/src/test/java/io/vertx/ext/web/handler/graphql/ApolloWSHandlerTest.java
var httpClient = vertx.createHttpClient(options);
httpClient.webSocket("/graphql")
.onSuccess(ws -> {
ws.closeHandler(v -> log.info("websocket is being closed"));
ws.endHandler(v -> log.info("websocket is being ended"));
ws.exceptionHandler(e -> log.info("catching websocket exception: {}", e.getMessage()));
ws.textMessageHandler(text -> {
//log.info("websocket message handler:{}", text);
JsonObject obj = new JsonObject(text);
ApolloWSMessageType type = ApolloWSMessageType.from(obj.getString("type"));
if (type.equals(CONNECTION_KEEP_ALIVE)) {
return;// do nothing when ka.
} else if (type.equals(DATA)) {
// handle the subscription `commentAdded` data.
log.info("subscription commentAdded data: {}", obj.getJsonObject("payload").getJsonObject("data").getJsonObject("commentAdded"));
}
});
JsonObject messageInit = new JsonObject()
.put("type", "connection_init")//this is required to initialize a connection.
.put("id", "1");
JsonObject message = new JsonObject()
.put("payload", new JsonObject()
.put("query", "subscription onCommentAdded { commentAdded { id content } }"))
.put("type", "start")
.put("id", "1");
ws.write(messageInit.toBuffer());
ws.write(message.toBuffer());
})
.onFailure(e -> log.error("error: {}", e));
addCommentToPost(client, id);
addCommentToPost(client, id);
addCommentToPost(client, id);
}

In the above addComment method, we have switch to use HttpClient to handle WebSocket request.

Firstly it opens a WebSocket connection to /graphql WebSocket endpoint, and then write the GraphQL request as message payload, and use a textMessageHandler callback hook to tap the WebSocket response from the server side.

Let’s have a look at the file uploads.

private void uploadFile(WebClient client) {
Buffer fileBuf = vertx.fileSystem().readFileBlocking("test.txt");
MultipartForm form = MultipartForm.create();
String query = """
mutation upload($file:Upload!){
upload(file:$file)
}
""";
var variables = new HashMap<String, Object>();
variables.put("file", null);
form.attribute("operations", Json.encode(Map.of("query", query, "variables", variables)));
form.attribute("map", Json.encode(Map.of("file0", List.of("variables.file"))));
form.textFileUpload("file0", "test.txt", fileBuf, "text/plain");
client.post("/graphql")
.sendMultipartForm(form)
.onSuccess(
data -> log.info("data of upload: {}", data.bodyAsString())
)
.onFailure(e -> log.error("error: {}", e));
}

As you see, it is very easy to create a multipart form and send it via sendMultipartForm directly with the advanced WebClient API.

Get the sample codes from my Github.

--

--

Self-employed technical consultant, solution architect and full-stack developer, open source contributor, freelancer and remote worker