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.