GraphQL with SmallRye
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus implements server-side GraphQL through the smallrye-graphql extension, an implementation of the
MicroProfile GraphQL specification: a schema is
derived from annotated CDI beans rather than hand-written in SDL, and the extension wires that schema into a
/graphql endpoint plus a Dev UI-integrated explorer. This page is deliberately narrow: it covers only the
Quarkus/SmallRye integration surface — the annotations, server wiring, clients, and Dev UI. GraphQL as a
language, schema design, pagination, security, and federation are not Quarkus-specific concerns and are covered
in depth by the dedicated GraphQL reference instead of being duplicated here.
Adding the extension
./mvnw quarkus:add-extension -Dextensions=smallrye-graphql
This adds io.quarkus:quarkus-smallrye-graphql to the project and, in dev mode, immediately makes a
/graphql endpoint and the Dev UI explorer available, even before a single @GraphQLApi bean is written.
The SmallRye GraphQL server programming model
@GraphQLApi, @Query and @Mutation
A GraphQL API is a plain CDI bean annotated @GraphQLApi, with one method per root Query or Mutation field.
Method return types become GraphQL output types and method parameters become GraphQL arguments — both are
derived from the Java types via reflection, the same way MicroProfile GraphQL implementations are specified to
behave:
package com.example.graphql;
import java.util.List;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.Query;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@GraphQLApi
@ApplicationScoped
public class BookResource {
@Inject
BookRepository repository;
@Query("books")
public List<Book> allBooks() {
return repository.listAll();
}
@Query("book")
public Book bookById(@Name("id") Long id) {
return repository.findById(id);
}
@Mutation("createBook")
public Book createBook(@Name("title") String title, @Name("authorId") Long authorId) {
return repository.create(title, authorId);
}
}
@Query and @Mutation methods behave exactly like a JAX-RS resource method in one respect: they run inside a
request-scoped context, so @Inject-ed CDI beans, transactions, and security annotations (@RolesAllowed, and
so on) all work the same way they would on a REST endpoint — GraphQL is an additional transport in front of the
same service layer, not a separate runtime.
@Subscription: push-based updates
A @Subscription method returns a Mutiny Multi<T> instead of a plain value; each item the Multi emits is
pushed to every subscribed client over the graphql-ws (or the legacy graphql-transport-ws) WebSocket
transport as it happens, rather than being polled for:
package com.example.graphql;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Subscription;
import io.smallrye.mutiny.Multi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@GraphQLApi
@ApplicationScoped
public class BookSubscriptions {
@Inject
BookEventBroker broker;
@Subscription("bookCreated")
public Multi<Book> bookCreated() {
return broker.streamCreatedBooks();
}
}
No extra application.properties entry is required for the WebSocket transport itself — it is exposed
automatically alongside the HTTP endpoint once at least one @Subscription method exists on the classpath. For
GraphQL subscriptions as a language-level concept (the operation type, event streams, and transport choices in
general), see Subscriptions in the GraphQL reference.
@Source: resolving additional fields
A @Source method adds a field to a type that is not declared on that type’s own class — useful for
computing a field lazily, or for attaching data from a different part of the domain model without polluting the
entity itself. The first parameter, annotated @Source, is the parent instance the field is being resolved for:
package com.example.graphql;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Source;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@GraphQLApi
@ApplicationScoped
public class AuthorFieldResolvers {
@Inject
AuthorRepository authorRepository;
public Author author(@Source Book book) {
return authorRepository.findById(book.getAuthorId());
}
}
Any query that selects author on a Book now triggers this resolver, once per Book instance in the result — which is exactly the shape of the N+1 problem addressed next.
Batching @Source resolvers to avoid N+1
A @Source method that takes a List of the parent type instead of a single instance is invoked once for
the whole result set rather than once per parent, and returns a correspondingly ordered List of results — this is SmallRye GraphQL’s batching mechanism, and it turns the N-queries-for-N-books problem above into a
single query:
package com.example.graphql;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Source;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@GraphQLApi
@ApplicationScoped
public class BatchedAuthorFieldResolvers {
@Inject
AuthorRepository authorRepository;
public List<Author> author(@Source List<Book> books) {
List<Long> authorIds = books.stream().map(Book::getAuthorId).collect(Collectors.toList());
Map<Long, Author> byId = authorRepository.findByIds(authorIds);
return books.stream().map(b -> byId.get(b.getAuthorId())).collect(Collectors.toList());
}
}
This is the Quarkus/SmallRye-specific answer to the N+1 problem; for the general pattern (and the DataLoader model it corresponds to in other GraphQL runtimes), see Performance & N+1 in the GraphQL reference.
Error handling
An exception thrown from a @Query, @Mutation, @Subscription, or @Source method does not fail the whole
HTTP response — it is caught and surfaced as an entry in the response’s errors array, alongside a data
object with null at the failed field’s position, per the GraphQL-over-HTTP conventions covered in
Response and Error Handling. By default, an
unrecognized exception’s message is hidden from clients and replaced with a generic message, to avoid leaking
internal detail; this can be adjusted per-exception with @GraphQLException or, for full control, by
implementing SmallRye GraphQL’s ExceptionHandlerProvider and registering it as a CDI bean:
package com.example.graphql;
import org.eclipse.microprofile.graphql.GraphQLException;
@GraphQLException
public class BookNotFoundException extends RuntimeException {
public BookNotFoundException(Long id) {
super("Book not found: " + id);
}
}
Annotating an exception @GraphQLException allows its message to reach clients as-is instead of being masked,
which is the usual choice for domain exceptions that are safe (and useful) to expose, such as a "not found" or
validation failure.
The GraphQL Dev UI (/q/graphql-ui)
In dev mode, the smallrye-graphql extension contributes a card to the
Dev UI that opens a GraphiQL-style explorer at
/q/graphql-ui: a schema browser, an editor with autocompletion against the live schema, and a panel for
running queries, mutations, and subscriptions directly against the running application — the GraphQL
counterpart to the REST endpoint browser on the same Dev UI. Like the rest of the Dev UI, /q/graphql-ui is a
dev-mode-only feature and is not part of a packaged application.
Consuming GraphQL from Quarkus
Quarkus also ships client support, for calling a GraphQL API — its own or a third party’s — from within a
Quarkus application, via the smallrye-graphql-client extension:
./mvnw quarkus:add-extension -Dextensions=smallrye-graphql-client
The typed GraphQL client
The typed client mirrors the way a REST client interface works in Quarkus: an interface annotated with the same
@Query/@Mutation/@Name annotations used on the server side describes the operations available, and
SmallRye GraphQL generates an implementation at build time that sends the corresponding GraphQL document over
the wire:
package com.example.graphql;
import java.util.List;
import org.eclipse.microprofile.graphql.GraphQLClientApi;
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.Query;
@GraphQLClientApi(configKey = "books-client")
public interface BooksClientApi {
@Query("books")
List<Book> allBooks();
@Query("book")
Book bookById(@Name("id") Long id);
}
The configKey ties the interface to a URL configured in application.properties:
quarkus.smallrye-graphql-client.books-client.url=https://api.example.com/graphql
and the generated client is injected like any other CDI bean:
package com.example.graphql;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder;
@ApplicationScoped
public class BooksClientProducer {
BooksClientApi client() {
return TypesafeGraphQLClientBuilder.newBuilder().build(BooksClientApi.class);
}
}
The typed client is the right default whenever the queries a project needs are known ahead of time — it gets the same compile-time safety a REST client gives for JSON payloads.
The dynamic GraphQL client
When the query shape is not known at compile time — built from user input, assembled conditionally, or aimed at a schema the project does not control closely enough to bind an interface to it — the dynamic client builds a GraphQL document programmatically at runtime instead:
package com.example.graphql;
import static io.smallrye.graphql.client.core.Document.document;
import static io.smallrye.graphql.client.core.Field.field;
import static io.smallrye.graphql.client.core.Operation.operation;
import io.smallrye.graphql.client.Response;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder;
import io.smallrye.graphql.client.core.Document;
public class DynamicBooksQuery {
public Response fetchBooks() throws Exception {
Document query = document(
operation(
field("books", field("id"), field("title"))));
try (DynamicGraphQLClient client = DynamicGraphQLClientBuilder.newBuilder()
.url("https://api.example.com/graphql")
.build()) {
return client.executeSync(query);
}
}
}
Response exposes the raw data and errors payload for the caller to navigate, which is the trade-off for
not binding to a fixed Java return type — there is no generated model to deserialize into automatically.
Client tooling in the Dev UI
Both the typed and dynamic clients configured in a project surface alongside the server-side GraphQL card
described earlier: the same /q/dev Dev UI lists configured GraphQL client endpoints next to the schema
explorer, so a project that both serves and consumes GraphQL sees both sides of the wire from one running dev
JVM.
For GraphQL as a language and its wider surface — the query language itself, schema design (types, interfaces, unions, inputs), pagination patterns, authorization and security, and federation — this page covers only the Quarkus/SmallRye integration; see the dedicated GraphQL reference for those concepts.