Supercharge Your Java APIs with GraphQL and Quarkus
Learn how to query and update data across services using GraphQL in Quarkus. With real examples, test coverage, and performance tips.
GraphQL has quickly become the preferred query language for modern APIs, solving over-fetching, under-fetching, and allowing developers to precisely request the data they need. Combined with Quarkus, a powerful Java framework designed for cloud-native applications, you have a potent mix for building performant, scalable applications. In this article, we'll dig into how you can use Quarkus with GraphQL to query and update data across multiple services. We'll also cover practical techniques for testing your GraphQL queries and mutations effectively. You can follow this step by step tutorial or go directly to the working example on my Github repository.
What is GraphQL and How Does It Work?
GraphQL is a query language and runtime for APIs developed by Facebook in 2012 and open-sourced in 2015. Unlike REST, where clients access different endpoints for different resources, GraphQL exposes a single endpoint and allows clients to query exactly the data they need in a single request.
The power of GraphQL lies in its schema and type system. A schema defines the structure of the data available to clients, including the relationships between different types. Clients use this schema to construct precise queries or mutations. Queries fetch data; mutations modify it.
For example, instead of making separate requests to /user/123
, /user/123/orders
, and /user/123/settings
, a GraphQL query might look like this:
{
user(id: "123") {
name
email
orders {
id
total
}
settings {
theme
}
}
}
This single query returns structured JSON data, shaped exactly to the client's needs, reducing over-fetching and minimizing the number of round trips to the server.
What Are Mutations?
While queries are used to retrieve data, mutations in GraphQL are used to modify server-side data. They follow a similar syntax but are explicitly marked with the mutation
keyword. Mutations also return data, typically reflecting the updated or newly created resource.
Here’s an example mutation that updates a user’s email address:
mutation {
updateUserEmail(id: "123", email: "newalice@example.com") {
id
name
email
}
}
The response would include the updated user object:
{
"data": {
"updateUserEmail": {
"id": "123",
"name": "Alice",
"email": "newalice@example.com"
}
}
}
This mutation illustrates the ability of GraphQL to not only perform complex reads but also structured and validated writes, all while maintaining strong typing and introspection.
Additionally, GraphQL schemas define all types, queries, and mutations available in the API. Tools like GraphiQL or Quarkus' GraphQL UI make it easy to explore and test APIs interactively.
Getting Started: Why GraphQL and Quarkus?
Before jumping into code, let's quickly understand the synergy between GraphQL and Quarkus. GraphQL allows clients to define exactly what data they need from the server, optimizing performance and reducing data transfer overhead. Quarkus complements this by providing a supremely efficient runtime, rapid startup, and excellent developer productivity tools—perfect for modern microservices architecture.
Step 1: Bootstrap Your Quarkus Project
Open a terminal and scaffold a new project with the necessary extensions:
mvn io.quarkus.platform:quarkus-maven-plugin:3.22.1:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=quarkus-graphql \
-Dextensions="rest-jackson, smallrye-graphql"
cd quarkus-graphql
You now have a Quarkus project with:
rest-jackson
for creating JSON-based REST endpoints.smallrye-graphql
for creating GraphQL APIs with Quarkus.
Step 2: Defining the GraphQL Schema
Let's build a schema that matches our earlier example: a user
with orders
and settings
.
Create the main GraphQL resource:
package org.acme;
import java.util.List;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.Query;
@GraphQLApi
public class UserResource {
@Query("user")
public User getUser(@Name("id") int id) {
return fetchUserWithDetails(id);
}
private User fetchUserWithDetails(int id) {
// Simulate data from multiple services
User user = new User(id, "Alice", "alice@example.com");
user.setOrders(List.of(new Order("o1", 49.99), new Order("o2", 29.95)));
user.setSettings(new Settings("dark"));
return user;
}
}
Now define the data classes:
public class User {
private int id;
private String name;
private String email;
private List<Order> orders;
private Settings settings;
// Constructors, getters, and setters
}
public class Order {
private String id;
private double total;
// Constructors, getters, and setters
}
public class Settings {
private String theme;
// Constructors, getters, and setters
}
Step 3: Implementing Mutations
GraphQL mutations allow clients to modify data. Let's allow updating the user email:
package org.acme;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Name;
@GraphQLApi
public class UserMutation {
@Mutation("updateUserEmail")
public User updateUserEmail(@Name("id") int id, @Name("email") String email) {
return updateEmailInService(id, email);
}
private User updateEmailInService(int id, String email) {
// Simulate a mutation result
return new User(id, email);
}
}
Step 4: Testing Your GraphQL APIs
Effective testing ensures your GraphQL APIs behave as expected. Quarkus integrates seamlessly with JUnit for testing GraphQL queries and mutations:
package org.acme;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.equalTo;
@QuarkusTest
public class GraphQLResourceTest {
@Test
void testQueryUserWithOrdersAndSettings() {
given()
.contentType("application/json")
.body("{\"query\":\"{ user(id: \\\"123\\\") { name email orders { id total } settings { theme } } }\"}")
.when()
.post("/graphql")
.then()
.statusCode(200)
.body("data.user.name", equalTo("Alice"))
.body("data.user.orders.size()", equalTo(2))
.body("data.user.settings.theme", equalTo("dark"));
}
@Test
void testMutationUpdateEmail() {
given()
.contentType("application/json")
.body("{\"query\":\"mutation { updateUserEmail(id: \\\"123\\\", email: \\\"newalice@example.com\\\") { email } }\"}")
.when()
.post("/graphql")
.then()
.statusCode(200)
.body("data.updateUserEmail.email", equalTo("newalice@example.com"));
}
}
Step 5: Running Your Application
Use Quarkus Dev mode for rapid feedback:
quarkus dev
Navigate to http://localhost:8080/q/graphql-ui
to interact with your API via a built-in GraphQL playground.
Navigate to http://localhost:8080/q/dev-ui/continuous-testing
to see your live tests in action with Quarkus continuous testing.
Handling Real-World Complexity
For real-world applications, leverage Quarkus' powerful REST Client Reactive and MicroProfile Fault Tolerance features to handle failures gracefully when fetching data from multiple services.
Optimizing Performance
GraphQL offers performance advantages, but improper data fetching can introduce latency. Use Quarkus' caching mechanisms, such as @CacheResult
, to reduce repeated calls and improve response times.
Documenting GraphQL APIs with Quarkus
GraphQL APIs don't use OpenAPI, but they are inherently self-documenting through the schema. The schema defines every type, query, mutation, and field available in the API, along with their arguments and return types. This makes it easy to introspect and explore your API without writing separate documentation files.
Introspection and Tooling
GraphQL servers automatically support introspection queries. Developer tools like GraphiQL or Quarkus' built-in GraphQL UI at /q/graphql-ui
leverage this to offer interactive documentation:
Explore the schema: See all available types, fields, and arguments.
Try queries and mutations: Execute test calls in a browser interface.
Understand types: View relationships and nested fields in real time.
Accessing the Schema
You can access your GraphQL schema definition directly in Quarkus at:
http://localhost:8080/graphql/schema.graphql
This can be useful for tooling or frontend teams that want to generate strongly-typed clients.
Adding Descriptions
You can add documentation to individual queries, mutations, and fields using @Description
from SmallRye GraphQL:
@GraphQLApi
public class UserResource {
@Query("user")
@Description("Fetch a user by ID including orders and settings")
public User getUser(@Name("id") String id) {
return ...;
}
}
These descriptions appear in the GraphQL UI and make it easier for consumers of your API to understand its capabilities.
Conclusion
Using GraphQL with Quarkus lets you build powerful, efficient APIs that aggregate and mutate data across multiple services with ease. By following structured testing practices, you ensure reliability and maintainability in your applications. Embrace these techniques to supercharge your development workflow and deliver outstanding user experiences.
Further Reading
Want to dive deeper into GraphQL with Quarkus? These official resources and community blogs will take you further:
Using SmallRye GraphQL with Quarkus
The official guide to using GraphQL in Quarkus. Covers schema creation, queries, mutations, and advanced configuration.Quarkus GraphQL Quickstart Example
A runnable GitHub example that demonstrates the basics of GraphQL in a Quarkus app.SmallRye GraphQL Documentation
The project powering GraphQL in Quarkus. Useful for learning about annotations, configuration, and advanced features.GraphQL Java
The low-level engine underneath SmallRye GraphQL, if you're curious about the internals or want to explore deeper integrations.