Consuming GraphQL APIs with Vertx HttpClient/WebClient
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.
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.