Consuming GraphQL APIs with Vert.x HttpClient/WebClient
In a previous post, we covered consuming RESTful APIs with Vert.x HttpClient and WebClient. In this post, we will consume the GraphQL APIs created in the graphql-http and graphql-transport-ws modules.
Vert.x does not provide a dedicated GraphQL client library like some other frameworks. Instead, you use the generic Vert.x HttpClient (low-level) or WebClient (high-level) to interact with GraphQL endpoints over HTTP. For WebSocket-based GraphQL (subscriptions), you use the Vert.x WebSocketClient.
Checkout the complete sample codes from my Github (tests) and graphql-transport-ws (tests).
GraphQL over HTTP
Refer to the GraphQL over HTTP specification and GraphQL multipart request specification for the wire format.
The request body follows this JSON structure:
And the response body is:
When an error occurs, the errors field contains error details. Unlike RESTful APIs, the HTTP response status code is typically 200 even when application-level errors occur.
Using HttpClient (low-level)
The following example is from the graphql-http test, which sends queries and mutations via POST /graphql:
@ExtendWith(VertxExtension.class)
@Slf4j
public class TestMainVerticle {
HttpClient client;
@BeforeEach
void setup(Vertx vertx, VertxTestContext testContext) {
vertx.deployVerticle(new MainVerticle())
.onComplete(testContext.succeeding(id -> {
var options = new HttpClientOptions()
.setDefaultHost("localhost")
.setDefaultPort(8080);
client = vertx.createHttpClient(options);
testContext.completeNow();
}));
}
@Test
void getAllPosts(Vertx vertx, VertxTestContext testContext) {
var query = """
query {
allPosts{
id
title
content
}
}""";
client.request(HttpMethod.POST, "/graphql")
.flatMap(req -> req.putHeader("Content-Type", "application/json")
.putHeader("Accept", "application/json")
.send(Json.encode(Map.of("query", query)))
.flatMap(HttpClientResponse::body)
)
.onComplete(testContext.succeeding(buffer ->
testContext.verify(() -> {
JsonArray array = buffer.toJsonObject()
.getJsonObject("data")
.getJsonArray("allPosts");
assertThat(array.size()).isGreaterThan(0);
testContext.completeNow();
})
));
}
}
The key aspects are:
1. Set Content-Type: application/json and Accept: application/json headers
2. Encode the request body as JSON with the query field (and optional variables and operationName)
3. Send via POST /graphql
Using WebClient (high-level)
WebClient provides a more fluent API for sending requests, including JSON payloads and multipart forms:
var options = new WebClientOptions()
.setUserAgent(WebClientOptions.loadUserAgent())
.setDefaultHost("localhost")
.setDefaultPort(8080);
var client = WebClient.create(vertx, options);
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));
Mutations with Variables
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 -> {
var createdId = data.bodyAsJsonObject()
.getJsonObject("data").getString("createPost");
log.info("created post id: {}", createdId);
})
.onFailure(e -> log.error("error: {}", e));
File Uploads
File uploads follow the GraphQL multipart request specification and use MultipartForm:
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)
}
""";
form.attribute("operations", Json.encode(Map.of("query", query, "variables", Map.of("file", null))));
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));
}
Using WebSocketClient for graphql-transport-ws
For the graphql-transport-ws variant, all operations flow through a WebSocket connection using the dedicated WebSocketClient:
var options = new WebSocketClientOptions()
.setConnectTimeout(7200)
.setDefaultHost("localhost")
.setDefaultPort(8080);
var client = vertx.createWebSocketClient(options);
var connectOptions = new WebSocketConnectOptions()
.addSubProtocol("graphql-transport-ws")
.setURI("/graphql");
client.connect(connectOptions)
.onSuccess(socket -> {
socket.textMessageHandler(text -> {
JsonObject obj = new JsonObject(text);
String type = obj.getString("type");
if (type.equals("next")) {
// handle subscription data
log.info("received: {}", obj.getJsonObject("payload"));
}
});
// Initialize connection
JsonObject init = new JsonObject()
.put("type", "connection_init");
socket.write(init.toBuffer());
// Subscribe to a mutation/query
JsonObject subscribe = new JsonObject()
.put("payload", new JsonObject()
.put("query", "mutation { createPost(createPostInput: {title: \"test\", content: \"content\"}) }")
)
.put("type", "subscribe")
.put("id", "1");
socket.write(subscribe.toBuffer());
});
See the graphql-transport-ws test for a complete working example.
Summary
| Transport | Client | Use Case |
|---|---|---|
HTTP (graphql-http) |
HttpClient / WebClient |
Queries, mutations, file uploads, subscriptions (via SSE) |
WebSocket (graphql-transport-ws) |
WebSocketClient |
All operations over WebSocket, real-time subscriptions |
Get the sample codes from my Github.