Spring for GraphQL provides support for Spring applications built on GraphQL Java . It is a joint collaboration between the GraphQL Java team and Spring engineering.
Spring for GraphQL is the successor of the GraphQL Java Spring project from the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications.
Please, use our issue tracker to report a problem, discuss a design issue, or to request a feature.
Check the Wiki . for what’s new, baseline requirements, and upgrade notes, and other cross-version information.
To get started, see the Boot Starter and Samples sections.
Spring for GraphQL supports server handling of GraphQL requests over HTTP, WebSocket, and RSocket.
GraphQlHttpHandler
handles GraphQL over HTTP requests and delegates to the
Interception
chain for request execution. There are two variants, one for
Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have
equivalent functionality, but rely on blocking vs non-blocking I/O respectively for
writing the HTTP response.
Requests must use HTTP POST with
"application/json"
as content type and GraphQL request details
included as JSON in the request body, as defined in the proposed
GraphQL over HTTP
specification.
Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK),
and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response.
The default and preferred choice of media type is
"application/graphql-response+json"
, but
"application/json"
is also supported, as described in the specification.
GraphQlHttpHandler
can be exposed as an HTTP endpoint by declaring a
RouterFunction
bean and using the
RouterFunctions
from Spring MVC or WebFlux to create the route. The
Boot Starter
does this, see the
Web Endpoints
section for
details, or check
GraphQlWebMvcAutoConfiguration
or
GraphQlWebFluxAutoConfiguration
it contains, for the actual config.
The 1.0.x branch of this repository contains a Spring MVC HTTP sample application.
As a protocol GraphQL focuses on the exchange of textual data. This doesn’t include binary data such as images, but there is a separate, informal graphql-multipart-request-spec that allows file uploads with GraphQL over HTTP.
Spring for GraphQL does not support the
graphql-multipart-request-spec
directly.
While the spec does provide the benefit of a unified GraphQL API, the actual experince has
led to a number of issues, and best practice recommendations have evolved, see
Apollo Server File Upload Best Practices
for a more detailed discussion.
If you would like to use
graphql-multipart-request-spec
in your application, you can
do so through the library
multipart-spring-graphql
.
GraphQlWebSocketHandler
handles GraphQL over WebSocket requests based on the
protocol
defined in the
graphql-ws
library. The main reason to use
GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL
responses, but it can also be used for regular queries with a single response.
The handler delegates every request to the
Interception
chain for further
request execution.
There are two such protocols, one in the subscriptions-transport-ws library and another in the graphql-ws library. The former is not active and succeeded by the latter. Read this blog post for the history.
There are two variants of
GraphQlWebSocketHandler
, one for Spring MVC and one for
Spring WebFlux. Both handle requests asynchronously and have equivalent functionality.
The WebFlux handler also uses non-blocking I/O and back pressure to stream messages,
which works well since in GraphQL Java a subscription response is a Reactive Streams
Publisher
.
The
graphql-ws
project lists a number of
recipes
for client use.
GraphQlWebSocketHandler
can be exposed as a WebSocket endpoint by declaring a
SimpleUrlHandlerMapping
bean and using it to map the handler to a URL path. By default,
the
Boot Starter
does not expose a GraphQL over WebSocket endpoint, but it’s easy to
enable it by adding a property for the endpoint path. Please, see the
Web Endpoints
section for details, or check the
GraphQlWebMvcAutoConfiguration
or the
GraphQlWebFluxAutoConfiguration
for the actual Boot starter config.
The 1.0.x branch of this repository contains a WebFlux WebSocket sample application.
GraphQlRSocketHandler
handles GraphQL over RSocket requests. Queries and mutations are
expected and handled as an RSocket
request-response
interaction while subscriptions are
handled as
request-stream
.
GraphQlRSocketHandler
can be used a delegate from an
@Controller
that is mapped to
the route for GraphQL requests. For example:
import java.util.Map; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.graphql.server.GraphQlRSocketHandler; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.stereotype.Controller; @Controller public class GraphQlRSocketController { private final GraphQlRSocketHandler handler; GraphQlRSocketController(GraphQlRSocketHandler handler) { this.handler = handler; @MessageMapping("graphql") public Mono<Map<String, Object>> handle(Map<String, Object> payload) { return this.handler.handle(payload); @MessageMapping("graphql") public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) { return this.handler.handleSubscription(payload);
2.4. Interception
Server transports allow intercepting requests before and after the GraphQL Java engine is called to process a request.
2.4.1.
WebGraphQlInterceptor
HTTP and WebSocket transports invoke a chain of 0 or more
import reactor.core.publisher.Mono; import org.springframework.graphql.data.method.annotation.ContextValue; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.graphql.server.WebGraphQlInterceptor; import org.springframework.graphql.server.WebGraphQlRequest; import org.springframework.graphql.server.WebGraphQlResponse; import org.springframework.stereotype.Controller; class RequestHeaderInterceptor implements WebGraphQlInterceptor { (1) @Override public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { String value = request.getHeaders().getFirst("myHeader"); request.configureExecutionInput((executionInput, builder) -> builder.graphQLContext(Collections.singletonMap("myHeader", value)).build()); return chain.next(request); @Controller class MyContextValueController { (2) @QueryMapping Person person(@ContextValue String myHeader) { import reactor.core.publisher.Mono; import org.springframework.graphql.data.method.annotation.QueryMapping; import org.springframework.graphql.server.WebGraphQlInterceptor; import org.springframework.graphql.server.WebGraphQlRequest; import org.springframework.graphql.server.WebGraphQlResponse; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Controller; // Subsequent access from a WebGraphQlInterceptor class ResponseHeaderInterceptor implements WebGraphQlInterceptor { @Override public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { (2) return chain.next(request).doOnNext(response -> { String value = response.getExecutionInput().getGraphQLContext().get("cookieName"); ResponseCookie cookie = ResponseCookie.from("cookieName", value).build(); response.getResponseHeaders().add(HttpHeaders.SET_COOKIE, cookie.toString()); @Controller class MyCookieController { @QueryMapping Person person(GraphQLContext context) { (1) context.put("cookieName", "123");WebGraphQlInterceptor
, followed by anExecutionGraphQlService
that calls the GraphQL Java engine.WebGraphQlInterceptor
allows an application to intercept incoming requests and do one of the following:
WebGraphQlHandler
can modify theExecutionResult
, for example, to inspect and modify request validation errors that are raised before execution begins and which cannot be handled with aDataFetcherExceptionResolver
:import java.util.List; import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import reactor.core.publisher.Mono; import org.springframework.graphql.server.WebGraphQlInterceptor; import org.springframework.graphql.server.WebGraphQlRequest; import org.springframework.graphql.server.WebGraphQlResponse; class RequestErrorInterceptor implements WebGraphQlInterceptor { @Override public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { return chain.next(request).map(response -> { if (response.isValid()) { return response; (1) List<GraphQLError> errors = response.getErrors().stream() (2) .map(error -> { GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError(); // ... return builder.build(); .toList(); return response.transform(builder -> builder.errors(errors).build()); (3)
Use
WebGraphQlHandler
to configure theWebGraphQlInterceptor
chain. This is supported by the Boot Starter, see Web Endpoints.2.4.2.
RSocketQlInterceptor
Similar to
WebGraphQlInterceptor
, anRSocketQlInterceptor
allows intercepting GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use this to customize thegraphql.ExecutionInput
and thegraphql.ExecutionResult
.
ExecutionGraphQlService
is the main Spring abstraction to call GraphQL Java to execute requests. Underlying transports, such as the HTTP, delegate toExecutionGraphQlService
to handle requests.The main implementation,
DefaultExecutionGraphQlService
, is configured with aGraphQlSource
for access to thegraphql.GraphQL
instance to invoke.3.1.
GraphQLSource
GraphQlSource
is a contract to expose thegraphql.GraphQL
instance to use that also includes a builder API to build that instance. The default builder is available viaGraphQlSource.schemaResourceBuilder()
.The Boot Starter creates an instance of this builder and further initializes it to load schema files from a configurable location, to expose properties to apply to
GraphQlSource.Builder
, to detectRuntimeWiringConfigurer
beans, Instrumentation beans for GraphQL metrics, andDataFetcherExceptionResolver
andSubscriptionExceptionResolver
beans for exception resolution. For further customizations, you can also declare aGraphQlSourceBuilderCustomizer
bean, for example:@Configuration(proxyBeanMethods = false) class GraphQlConfig { @Bean public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() { return (builder) -> builder.configureGraphQl(graphQlBuilder -> graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
3.1.1. Schema Resources
GraphQlSource.Builder
can be configured with one or moreResource
instances to be parsed and merged together. That means schema files can be loaded from just about any location.By default, the Boot starter looks for schema files with extensions ".graphqls" or ".gqls" under the location
classpath:graphql/**
, which is typicallysrc/main/resources/graphql
. You can also use a file system location, or any location supported by the SpringResource
hierarchy, including a custom implementation that loads schema files from remote locations, from storage, or from memory.3.1.2. Schema Creation
By default,
GraphQlSource.Builder
uses the GraphQL JavaSchemaGenerator
to create thegraphql.schema.GraphQLSchema
. This works for typical use, but if you need to use a different generator, e.g. for federation, you can register aschemaFactory
callback:GraphQlSource.Builder builder = ... builder.schemaResources(..) .configureRuntimeWiring(..) .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> { // create GraphQLSchema
GraphQL Java, server applications use Jackson only for serialization to and from maps of data. Client input is parsed into a map. Server output is assembled into a map based on the field selection set. This means you can’t rely on Jackson serialization/deserialization annotations. Instead, you can use custom scalar types.
DataFetcher
for a field although applications will typically use Annotated Controllers, and those are detected and registered asDataFetcher
s byAnnotatedControllerConfigurer
, which is aRuntimeWiringConfigurer
. The Boot Starter automatically registersAnnotatedControllerConfigurer
.The Boot Starter detects beans of type
RuntimeWiringConfigurer
and registers them in theGraphQlSource.Builder
. That means in most cases, you’ll' have something like the following in your configuration:@Configuration public class GraphQlConfig { @Bean public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) { GraphQLScalarType scalarType = ... ; SchemaDirectiveWiring directiveWiring = ... ; DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single(); return wiringBuilder -> wiringBuilder .scalar(scalarType) .directiveWiring(directiveWiring) .type("Query", builder -> builder.dataFetcher("book", dataFetcher));
If you need to add a
WiringFactory
, e.g. to make registrations that take into account schema definitions, implement the alternativeconfigure
method that accepts both theRuntimeWiring.Builder
and an outputList<WiringFactory>
. This allows you to add any number of factories that are then invoked in sequence.3.1.4.
TypeResolver
GraphQlSource.Builder
registersClassNameTypeResolver
as the defaultTypeResolver
to use for GraphQL Interfaces and Unions that don’t already have such a registration through aRuntimeWiringConfigurer
. The purpose of aTypeResolver
in GraphQL Java is to determine the GraphQL Object type for values returned from theDataFetcher
for a GraphQL Interface or Union field.
ClassNameTypeResolver
tries to match the simple class name of the value to a GraphQL Object Type and if it is not successful, it also navigates its super types including base classes and interfaces, looking for a match.ClassNameTypeResolver
provides an option to configure a name extracting function along withClass
to GraphQL Object type name mappings that should help to cover more corner cases:GraphQlSource.Builder builder = ... ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver(); classNameTypeResolver.setClassNameExtractor((klass) -> { // Implement Custom ClassName Extractor here builder.defaultTypeResolver(classNameTypeResolver);
3.1.5. Directives
The GraphQL language supports directives that "describe alternate runtime execution and type validation behavior in a GraphQL document". Directives are similar to annotations in Java but declared on types, fields, fragments and operations in a GraphQL document.
GraphQL Java provides the
SchemaDirectiveWiring
contract to help applications detect and handle directives. For more details, see Schema Directives in the GraphQL Java documentation.In Spring GraphQL you can register a
SchemaDirectiveWiring
through aRuntimeWiringConfigurer
. The Boot Starter detects such beans, so you might have something like:@Configuration public class GraphQlConfig { @Bean public RuntimeWiringConfigurer runtimeWiringConfigurer() { return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
You can register a
graphql.schema.GraphQLTypeVisitor
viabuilder.schemaResources(..).typeVisitorsToTransformSchema(..)
if you want to traverse and transform the schema after it is created, and make changes to the schema. Keep in mind that this is more expensive than Schema Traversal so generally prefer traversal to transformation unless you need to make schema changes.3.1.7. Schema Traversal
You can register a
graphql.schema.GraphQLTypeVisitor
viabuilder.schemaResources(..).typeVisitors(..)
if you want to traverse the schema after it is created, and possibly apply changes to theGraphQLCodeRegistry
. Keep in mind, however, that such a visitor cannot change the schema. See Schema Transformation, if you need to make changes to the schema.3.1.8. Schema Mapping Inspection
If a query, mutation, or subscription operation does not have a
DataFetcher
, it won’t return any data, and won’t do anything useful. Likewise, fields on schema types returned by an operation that are covered neither explicitly through aDataFetcher
registration, nor implicitly by the defaultPropertyDataFetcher
, which looks for a matching Java object property, will always benull
.GraphQL Java does not perform checks to ensure every schema field is covered, and that can result in gaps that might not be discovered depending on test coverage. At runtime you may get a "silent"
null
, or an error if the field is not nullable. As a lower level library, GraphQL Java simply does not know enough aboutDataFetcher
implementations and their return types, and therefore can’t compare schema type structure against Java object structure.Spring for GraphQL defines the
SelfDescribingDataFetcher
interface to allow aDataFetcher
to expose return type information. All SpringDataFetcher
implementations implement this interface. That includes those for Annotated Controllers, and those for Querydsl and Query by Example Spring Data repositories. For annotated controllers, the return type is derived from the declared return type on a@SchemaMapping
method.On startup, Spring for GraphQL can inspect schema fields,
DataFetcher
registrations, and the properties of Java objects returned fromDataFetcher
implementations to check if all schema fields are covered either by an explicitly registeredDataFetcher
, or a matching Java object property. The inspection also performs a reverse check looking forDataFetcher
registrations against schema fields that don’t exist.To enable inspection of schema mappings:
GraphQlSource.Builder builder = ... builder.schemaResources(..) .inspectSchemaMappings(report -> { logger.debug(report);
GraphQL schema inspection: Unmapped fields: {Book=[title], Author[firstName, lastName]} (1) Unmapped registrations: {Book.reviews=BookController#reviews[1 args]} (2) Skipped types: [BookOrAuthor] (3)There are limits to what schema field inspection can do, in particular when there is insufficient Java type information. This is the case if an annotated controller method is declared to return
java.lang.Object
, or if the return type has an unspecified generic parameter such asList<?>
, or if theDataFetcher
does not implementSelfDescribingDataFetcher
and the return type is not even known. In such cases, the Java object type structure remains unknown, and the schema type is listed as skipped in the resulting report. For every skipped type, a DEBUG message is logged to indicate why it was skipped.Schema union types are always skipped because there is no way for a controller method to declare such a return type in Java, and the Java type structure is unknown.
Schema interface types are supported only as far as fields declared directly, which are compared against properties on the Java type declared by a
SelfDescribingDataFetcher
. Additional fields on concrete implementations are not inspected. This could be improved in a future release to also inspect schemainterface
implementation types and to try to find a match among subtypes of the declared Java return type.3.1.9. Operation Caching
GraphQL Java must parse and validate an operation before executing it. This may impact performance significantly. To avoid the need to re-parse and validate, an application may configure a
PreparsedDocumentProvider
that caches and reuses Document instances. The GraphQL Java docs provide more details on query caching through aPreparsedDocumentProvider
.In Spring GraphQL you can register a
PreparsedDocumentProvider
throughGraphQlSource.Builder#configureGraphQl
:// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer GraphQlSource.Builder builder = ... // Create provider PreparsedDocumentProvider provider = ... builder.schemaResources(..) .configureRuntimeWiring(..) .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
3.2. Reactive
DataFetcher
The default
GraphQlSource
builder enables support for aDataFetcher
to returnMono
orFlux
which adapts those to aCompletableFuture
whereFlux
values are aggregated and turned into a List, unless the request is a GraphQL subscription request, in which case the return value remains a Reactive StreamsPublisher
for streaming GraphQL responses.A reactive
DataFetcher
can rely on access to Reactor context propagated from the transport layer, such as from a WebFlux request handling, see WebFlux Context.3.3. Context Propagation
Spring for GraphQL provides support to transparently propagate context from the HTTP, through GraphQL Java, and to
DataFetcher
and other components it invokes. This includes bothThreadLocal
context from the Spring MVC request handling thread and ReactorContext
from the WebFlux processing pipeline.3.3.1. WebMvc
A
DataFetcher
and other components invoked by GraphQL Java may not always execute on the same thread as the Spring MVC handler, for example if an asynchronousWebGraphQlInterceptor
orDataFetcher
switches to a different thread.Spring for GraphQL supports propagating
ThreadLocal
values from the Servlet container thread to the thread aDataFetcher
and other components invoked by GraphQL Java to execute on. To do this, an application needs to implementio.micrometer.context.ThreadLocalAccessor
for aThreadLocal
values of interest:public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> { @Override public Object key() { return RequestAttributesAccessor.class.getName(); @Override public RequestAttributes getValue() { return RequestContextHolder.getRequestAttributes(); @Override public void setValue(RequestAttributes attributes) { RequestContextHolder.setRequestAttributes(attributes); @Override public void reset() { RequestContextHolder.resetRequestAttributes();
You can register a
ThreadLocalAccessor
manually on startup with the globalContextRegistry
instance, which is accessible viaio.micrometer.context.ContextRegistry#getInstance()
. You can also register it automatically through thejava.util.ServiceLoader
mechanism.3.3.2. WebFlux
A Reactive
DataFetcher
can rely on access to Reactor context that originates from the WebFlux request handling chain. This includes Reactor context added by WebGraphQlInterceptor components.3.4. Exceptions
In GraphQL Java,
DataFetcherExceptionHandler
decides how to represent exceptions from data fetching in the "errors" section of the response. An application can register a single handler only.Spring for GraphQL registers a
DataFetcherExceptionHandler
that provides default handling and enables theDataFetcherExceptionResolver
contract. An application can register any number of resolvers viaGraphQLSource
builder and those are in order until one them resolves theException
to aList<graphql.GraphQLError>
. The Spring Boot starter detects beans of this type.
DataFetcherExceptionResolverAdapter
is a convenient base class with protected methodsresolveToSingleError
andresolveToMultipleErrors
.The Annotated Controllers programming model enables handling data fetching exceptions with annotated exception handler methods with a flexible method signature, see
@GraphQlExceptionHandler
for details.A
GraphQLError
can be assigned to a category based on the GraphQL Javagraphql.ErrorClassification
, or the Spring GraphQLErrorType
, which defines the following:If an exception remains unresolved, by default it is categorized as an
INTERNAL_ERROR
with a generic message that includes the category name and theexecutionId
fromDataFetchingEnvironment
. The message is intentionally opaque to avoid leaking implementation details. Applications can use aDataFetcherExceptionResolver
to customize error details.Unresolved exception are logged at ERROR level along with the
executionId
to correlate to the error sent to the client. Resolved exceptions are logged at DEBUG level.3.4.1. Request Exceptions
The GraphQL Java engine may run into validation or other errors when parsing the request and that in turn prevent request execution. In such cases, the response contains a "data" key with
null
and one or more request-level "errors" that are global, i.e. not having a field path.
DataFetcherExceptionResolver
cannot handle such global errors because they are raised before execution begins and before anyDataFetcher
is invoked. An application can use transport level interceptors to inspect and transform errors in theExecutionResult
. See examples underWebGraphQlInterceptor
.3.4.2. Subscription Exceptions
The
Publisher
for a subscription request may complete with an error signal in which case the underlying transport (e.g. WebSocket) sends a final "error" type message with a list of GraphQL errors.
DataFetcherExceptionResolver
cannot resolve errors from a subscriptionPublisher
, since the dataDataFetcher
only creates thePublisher
initially. After that, the transport subscribes to thePublisher
that may then complete with an error.An application can register a
SubscriptionExceptionResolver
in order to resolve exceptions from a subscriptionPublisher
in order to resolve those to GraphQL errors to send to the client.The GraphQL Cursor Connection specification defines a way to navigate large result sets by returning a subset of items at a time where each item is paired with a cursor that clients can use to request more items before or after the referenced item.
The specification calls the pattern "Connections". A schema type with a name that ends on Connection is a Connection Type that represents a paginated result set. All
~Connection
types contain an "edges" field where~Edge
type pairs the actual item with a cursor, as well as a "pageInfo" field with boolean flags to indicate if there are more items forward and backward.3.5.1. Connection Types
Connection
type definitions must be created for every type that needs pagination, adding boilerplate and noise to the schema. Spring for GraphQL providesConnectionTypeDefinitionConfigurer
to add these types on startup, if not already present in the parsed schema files. That means in the schema you only need this:Query { books(first:Int, after:String, last:Int, before:String): BookConnection type Book { id: ID! title: String!
Note the spec-defined forward pagination arguments
first
andafter
that clients can use to request the first N items after the given cursor, whilelast
andbefore
are backward pagination arguments to request the last N items before the given cursor.Next, configure
ConnectionTypeDefinitionConfigurer
as follows:
GraphQlSource.schemaResourceBuilder() .schemaResources(..) .typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
Once Connection Types are available in the schema, you also need equivalent Java types. GraphQL Java provides those, including generic
Connection
andEdge
, as well as aPageInfo
.One option is to populate a
Connection
and return it from your controller method orDataFetcher
. However, this requires boilerplate code to create theConnection
, creating cursors, wrapping each item as anEdge
, and creating thePageInfo
. Moreover, you may already have an underlying pagination mechanism such as when using Spring Data repositories.Spring for GraphQL defines the
ConnectionAdapter
contract to adapt a container of items toConnection
. Adapters are applied through aDataFetcher
decorator that is in turn installed through aConnectionFieldTypeVisitor
. You can configure it as follows:ConnectionAdapter adapter = ... ; GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) (1) GraphQlSource.schemaResourceBuilder() .schemaResources(..) .typeDefinitionConfigurer(..) .typeVisitors(List.of(visitor)) (2)
There are built-in
ConnectionAdapter
s for Spring Data’sWindow
andSlice
. You can also create your own custom adapter.ConnectionAdapter
implementations rely on aCursorStrategy
to create cursors for returned items. The same strategy is also used to support theSubrange
controller method argument that contains pagination input.3.5.3.
CursorStrategy
CursorStrategy
is a contract to encode and decode a String cursor that refers to the position of an item within a large result set. The cursor can be based on an index or on a keyset.A
ConnectionAdapter
uses this to encode cursors for returned items. Annotated Controllers methods, Querydsl repositories, and Query by Example repositories use it to decode cursors from pagination requests, and create aSubrange
.
CursorEncoder
is a related contract that further encodes and decodes String cursors to make them opaque to clients.EncodingCursorStrategy
combinesCursorStrategy
with aCursorEncoder
. You can useBase64CursorEncoder
,NoOpEncoder
or create your own.There is a built-in
CursorStrategy
for the Spring DataScrollPosition
. The Boot Starter registers aCursorStrategy<ScrollPosition>
withBase64Encoder
when Spring Data is present.3.5.4. Sort
There is no standard way to provide sort information in a GraphQL request. However, pagination depends on a stable sort order. You can use a default order, or otherwise expose input types and extract sort details from GraphQL arguments.
There is built-in support for Spring Data’s
Sort
as a controller method argument. For this to work, you need to have aSortStrategy
bean.3.6. Batch Loading
Given a
Book
and itsAuthor
, we can create oneDataFetcher
for a book and another for its author. This allows selecting books with or without authors, but it means books and authors aren’t loaded together, which is especially inefficient when querying multiple books as the author for each book is loaded individually. This is known as the N+1 select problem.3.6.1.
DataLoader
GraphQL Java provides a
DataLoader
mechanism for batch loading of related entities. You can find the full details in the GraphQL Java docs. Below is a summary of how it works:Register
DataLoader
's in theDataLoaderRegistry
that can load entities, given unique keys.
DataFetcher
's can accessDataLoader
's and use them to load entities by id.A
DataLoader
defers loading by returning a future so it can be done in a batch.
DataLoader
's maintain a per request cache of loaded entities that can further improve efficiency.3.6.2.
BatchLoaderRegistry
The complete batching loading mechanism in GraphQL Java requires implementing one of several
BatchLoader
interface, then wrapping and registering those asDataLoader
s with a name in theDataLoaderRegistry
.The API in Spring GraphQL is slightly different. For registration, there is only one, central
BatchLoaderRegistry
exposing factory methods and a builder to create and register any number of batch loading functions:@Configuration public class MyConfig { public MyConfig(BatchLoaderRegistry registry) { registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { // return Mono<Map<Long, Author> // more registrations ...
The Boot Starter declares a
BatchLoaderRegistry
bean that you can inject into your configuration, as shown above, or into any component such as a controller in order register batch loading functions. In turn theBatchLoaderRegistry
is injected intoDefaultExecutionGraphQlService
where it ensuresDataLoader
registrations per request.By default, the
DataLoader
name is based on the class name of the target entity. This allows an@SchemaMapping
method to declare a DataLoader argument with a generic type, and without the need for specifying a name. The name, however, can be customized through theBatchLoaderRegistry
builder, if necessary, along with otherDataLoaderOptions
.To configure default
DataLoaderOptions
globally, to use as a starting point for any registration, you can override Boot’sBatchLoaderRegistry
bean and use the constructor forDefaultBatchLoaderRegistry
that acceptsSupplier<DataLoaderOptions>
.For many cases, when loading related entities, you can use @BatchMapping controller methods, which are a shortcut for and replace the need to use
BatchLoaderRegistry
andDataLoader
directly.
BatchLoaderRegistry
provides other important benefits too. It supports access to the sameGraphQLContext
from batch loading functions and from@BatchMapping
methods, as well as ensures Context Propagation to them. This is why applications are expected to use it. It is possible to perform your ownDataLoader
registrations directly but such registrations would forgo the above benefits.3.6.3. Testing Batch Loading
Start by having
BatchLoaderRegistry
perform registrations on aDataLoaderRegistry
:BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry(); // perform registrations... DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build(); batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);
DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName()); loader.load(1L); loader.loadMany(Arrays.asList(2L, 3L)); List<Book> books = loader.dispatchAndJoin(); // actual loading assertThat(books).hasSize(3); assertThat(books.get(0).getName()).isEqualTo("..."); // ...
Spring for GraphQL lets you leverage existing Spring technology, following common programming models to expose underlying data sources through GraphQL.
This section discusses an integration layer for Spring Data that provides an easy way to adapt a Querydsl or a Query by Example repository to a
DataFetcher
, including the option for automated detection and GraphQL Query registration for repositories marked with@GraphQlRepository
.
4.1. Querydsl
Spring for GraphQL supports use of Querydsl to fetch data through the Spring Data Querydsl extension. Querydsl provides a flexible yet typesafe approach to express query predicates by generating a meta-model using annotation processors.
For example, declare a repository as
QuerydslPredicateExecutor
:public interface AccountRepository extends Repository<Account, Long>, QuerydslPredicateExecutor<Account> {
// For single result queries DataFetcher<Account> dataFetcher = QuerydslDataFetcher.builder(repository).single(); // For multi-result queries DataFetcher<Iterable<Account>> dataFetcher = QuerydslDataFetcher.builder(repository).many(); // For paginated queries DataFetcher<Iterable<Account>> dataFetcher = QuerydslDataFetcher.builder(repository).scrollable();
If the repository is
ReactiveQuerydslPredicateExecutor
, the builder returnsDataFetcher<Mono<Account>>
orDataFetcher<Flux<Account>>
. Spring Data supports this variant for MongoDB and Neo4j.4.1.1. Build Setup
To configure Querydsl in your build, follow the official reference documentation:
For example:
Gradledependencies { //... annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa", 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final', 'javax.annotation:javax.annotation-api:1.3.2' compileJava { options.annotationProcessorPath = configurations.annotationProcessor <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <classifier>jpa</classifier> <scope>provided</scope> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> <version>1.0.2.Final</version> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency> </dependencies> <plugins> <!-- Annotation processor configuration --> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>${apt-maven-plugin.version}</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins>
The webmvc-http sample uses Querydsl for
artifactRepositories
.4.1.2. Customizations
QuerydslDataFetcher
supports customizing how GraphQL arguments are bound onto properties to create a QuerydslPredicate
. By default, arguments are bound as "is equal to" for each available property. To customize that, you can useQuerydslDataFetcher
builder methods to provide aQuerydslBinderCustomizer
.A repository may itself be an instance of
QuerydslBinderCustomizer
. This is auto-detected and transparently applied during Auto-Registration. However, when manually building aQuerydslDataFetcher
you will need to use builder methods to apply it.
QuerydslDataFetcher
supports interface and DTO projections to transform query results before returning these for further GraphQL processing.To use Spring Data projections with Querydsl repositories, create either a projection interface or a target DTO class and configure it through the
projectAs
method to obtain aDataFetcher
producing the target type:class Account { String name, identifier, description; Person owner; interface AccountProjection { String getName(); String getIdentifier(); // For single result queries DataFetcher<AccountProjection> dataFetcher = QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); // For multi-result queries DataFetcher<Iterable<AccountProjection>> dataFetcher = QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.1.3. Auto-Registration
If a repository is annotated with
@GraphQlRepository
, it is automatically registered for queries that do not already have a registeredDataFetcher
and whose return type matches that of the repository domain type. This includes single value queries, multi-value queries, and paginated queries.By default, the name of the GraphQL type returned by the query must match the simple name of the repository domain type. If needed, you can use the
typeName
attribute of@GraphQlRepository
to specify the target GraphQL type name.For paginated queries, the simple name of the repository domain type must match the
Connection
type name without theConnection
ending (e.g.Book
matchesBooksConnection
). For auto-registration, pagination is offset-based with 20 items per page.Auto-registration detects if a given repository implements
QuerydslBinderCustomizer
and transparently applies that throughQuerydslDataFetcher
builder methods.Auto-registration is performed through a built-in
RuntimeWiringConfigurer
that can be obtained fromQuerydslDataFetcher
. The Boot Starter automatically detects@GraphQlRepository
beans and uses them to initialize theRuntimeWiringConfigurer
with.Auto-registration applies customizations by calling
customize(Builder)
on the repository instance if your repository implementsQuerydslBuilderCustomizer
orReactiveQuerydslBuilderCustomizer
respectively.Spring Data supports the use of Query by Example to fetch data. Query by Example (QBE) is a simple querying technique that does not require you to write queries through store-specific query languages.
Start by declaring a repository that is
QueryByExampleExecutor
:public interface AccountRepository extends Repository<Account, Long>, QueryByExampleExecutor<Account> {
// For single result queries DataFetcher<Account> dataFetcher = QueryByExampleDataFetcher.builder(repository).single(); // For multi-result queries DataFetcher<Iterable<Account>> dataFetcher = QueryByExampleDataFetcher.builder(repository).many(); // For paginated queries DataFetcher<Iterable<Account>> dataFetcher = QueryByExampleDataFetcher.builder(repository).scrollable();
The
DataFetcher
uses the GraphQL arguments map to create the domain type of the repository and use that as the example object to fetch data with. Spring Data supportsQueryByExampleDataFetcher
for JPA, MongoDB, Neo4j, and Redis.If the repository is
ReactiveQueryByExampleExecutor
, the builder returnsDataFetcher<Mono<Account>>
orDataFetcher<Flux<Account>>
. Spring Data supports this variant for MongoDB, Neo4j, Redis, and R2dbc.4.2.1. Build Setup
Query by Example is already included in the Spring Data modules for the data stores where it is supported, so no extra setup is required to enable it.
4.2.2. Customizations
QueryByExampleDataFetcher
supports interface and DTO projections to transform query results before returning these for further GraphQL processing.To use Spring Data projections with Query by Example repositories, create either a projection interface or a target DTO class and configure it through the
projectAs
method to obtain aDataFetcher
producing the target type:class Account { String name, identifier, description; Person owner; interface AccountProjection { String getName(); String getIdentifier(); // For single result queries DataFetcher<AccountProjection> dataFetcher = QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); // For multi-result queries DataFetcher<Iterable<AccountProjection>> dataFetcher = QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many();
4.2.3. Auto-Registration
If a repository is annotated with
@GraphQlRepository
, it is automatically registered for queries that do not already have a registeredDataFetcher
and whose return type matches that of the repository domain type. This includes single value queries, multi-value queries, and paginated queries.By default, the name of the GraphQL type returned by the query must match the simple name of the repository domain type. If needed, you can use the
typeName
attribute of@GraphQlRepository
to specify the target GraphQL type name.For paginated queries, the simple name of the repository domain type must match the
Connection
type name without theConnection
ending (e.g.Book
matchesBooksConnection
). For auto-registration, pagination is offset-based with 20 items per page.Auto-registration is performed through a built-in
RuntimeWiringConfigurer
that can be obtained fromQueryByExampleDataFetcher
. The Boot Starter automatically detects@GraphQlRepository
beans and uses them to initialize theRuntimeWiringConfigurer
with.Auto-registration applies customizations by calling
customize(Builder)
on the repository instance if your repository implementsQueryByExampleBuilderCustomizer
orReactiveQueryByExampleBuilderCustomizer
respectively.4.3. Selection Set vs Projections
A common question that arises is, how GraphQL selection sets compare to Spring Data projections and what role does each play?
The short answer is that Spring for GraphQL is not a data gateway that translates GraphQL queries directly into SQL or JSON queries. Instead, it lets you leverage existing Spring technology and does not assume a one for one mapping between the GraphQL schema and the underlying data model. That is why client-driven selection and server-side transformation of the data model can play complementary roles.
To better understand, consider that Spring Data promotes domain-driven (DDD) design as the recommended approach to manage complexity in the data layer. In DDD, it is important to adhere to the constraints of an aggregate. By definition an aggregate is valid only if loaded in its entirety, since a partially loaded aggregate may impose limitations on aggregate functionality.
In Spring Data you can choose whether you want your aggregate be exposed as is, or whether to apply transformations to the data model before returning it as a GraphQL result. Sometimes it’s enough to do the former, and by default the Querydsl and the Query by Example integrations turn the GraphQL selection set into property path hints that the underlying Spring Data module uses to limit the selection.
In other cases, it’s useful to reduce or even transform the underlying data model in order to adapt to the GraphQL schema. Spring Data supports this through Interface and DTO Projections.
Interface projections define a fixed set of properties to expose where properties may or may not be
null
, depending on the data store query result. There are two kinds of interface projections both of which determine what properties to load from the underlying data source:Closed interface projections are helpful if you cannot partially materialize the aggregate object, but you still want to expose a subset of properties.
Open interface projections leverage Spring’s
@Value
annotation and SpEL expressions to apply lightweight data transformations, such as concatenations, computations, or applying static functions to a property.DTO projections offer a higher level of customization as you can place transformation code either in the constructor or in getter methods.
DTO projections materialize from a query where the individual properties are determined by the projection itself. DTO projections are commonly used with full-args constructors (e.g. Java records), and therefore they can only be constructed if all required fields (or columns) are part of the database query result.
4.4. Scroll
As explained in Pagination, the GraphQL Cursor Connection spec defines a mechanism for pagination with
Connection
,Edge
, andPageInfo
schema types, while GraphQL Java provides the equivalent Java type representations.Spring for GraphQL provides built-in
ConnectionAdapter
implementations to adapt the Spring Data pagination typesWindow
andSlice
transparently. You can configure that as follows:CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder( new ScrollPositionCursorStrategy(), CursorEncoder.base64()); (1) GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of( new WindowConnectionAdapter(strategy), new SliceConnectionAdapter(strategy))); (2) GraphQlSource.schemaResourceBuilder() .schemaResources(..) .typeDefinitionConfigurer(..) .typeVisitors(List.of(visitor)); (3)
On the request side, a controller method can declare a ScrollSubrange method argument to paginate forward or backward. For this to work, you must declare a
CursorStrategy
supportsScrollPosition
as a bean.The Boot Starter declares a
CursorStrategy<ScrollPosition>
bean, and registers theConnectionFieldTypeVisitor
as shown above if Spring Data is on the classpath.4.5. Keyset Position
For
KeysetScrollPosition
, the cursor needs to be created from a keyset, which is essentially aMap
of key-value pairs. To decide how to create a cursor from a keyset, you can configureScrollPositionCursorStrategy
withCursorStrategy<Map<String, Object>>
. By default,JsonKeysetCursorStrategy
writes the keysetMap
to JSON. That works for simple like String, Boolean, Integer, and Double, but others cannot be restored back to the same type without target type information. The Jackson library has a default typing feature that can include type information in the JSON. To use it safely you must specify a list of allowed types. For example:PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder() .allowIfBaseType(Map.class) .allowIfSubType(ZonedDateTime.class) .build(); ObjectMapper mapper = new ObjectMapper(); mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL); CodecConfigurer configurer = ServerCodecConfigurer.create(); configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); JsonKeysetCursorStrategy strategy = new JsonKeysetCursorStrategy(configurer);
By default, if
JsonKeysetCursorStrategy
is created without aCodecConfigurer
and the Jackson library is on the classpath, customizations like the above are applied forDate
,Calendar
, and any type fromjava.time
.
4.6. Sort
Spring for GraphQL defines a
SortStrategy
to createSort
from GraphQL arguments.AbstractSortStrategy
implements the contract with abstract methods to extract the sort direction and properties. To enable support forSort
as a controller method argument, you need to declare aSortStrategy
bean.Spring for GraphQL provides an annotation-based programming model where
@Controller
components use annotations to declare handler methods with flexible method signatures to fetch the data for specific GraphQL fields. For example:@Controller public class GreetingController { @QueryMapping (1) public String hello() { (2) return "Hello, world!";
Spring for GraphQL uses
RuntimeWiring.Builder
to register the above handler method as agraphql.schema.DataFetcher
for the query named "hello".
5.1. Declaration
You can define
@Controller
beans as standard Spring bean definitions. The@Controller
stereotype allows for auto-detection, aligned with Spring general support for detecting@Controller
and@Component
classes on the classpath and auto-registering bean definitions for them. It also acts as a stereotype for the annotated class, indicating its role as a data fetching component in a GraphQL application.
AnnotatedControllerConfigurer
detects@Controller
beans and registers their annotated handler methods asDataFetcher
s viaRuntimeWiring.Builder
. It is an implementation ofRuntimeWiringConfigurer
which can be added toGraphQlSource.Builder
. The Boot Starter automatically declaresAnnotatedControllerConfigurer
as a bean and adds allRuntimeWiringConfigurer
beans toGraphQlSource.Builder
and that enables support for annotatedDataFetcher
s, see the GraphQL RuntimeWiring section in the Boot starter documentation.5.2.
@SchemaMapping
The
@SchemaMapping
annotation maps a handler method to a field in the GraphQL schema and declares it to be theDataFetcher
for that field. The annotation can specify the parent type name, and the field name:@Controller public class BookController { @SchemaMapping(typeName="Book", field="author") public Author getAuthor(Book book) { // ...
The
@SchemaMapping
annotation can also leave out those attributes, in which case the field name defaults to the method name, while the type name defaults to the simple class name of the source/parent object injected into the method. For example, the below defaults to type "Book" and field "author":
@Controller public class BookController { @SchemaMapping public Author author(Book book) { // ...
@QueryMapping
,@MutationMapping
, and@SubscriptionMapping
are meta annotations that are themselves annotated with@SchemaMapping
and have the typeName preset toQuery
,Mutation
, orSubscription
respectively. Effectively, these are shortcut annotations for fields under the Query, Mutation, and Subscription types respectively. For example:
@Controller public class BookController { @QueryMapping public Book bookById(@Argument Long id) { // ... @MutationMapping public Book addBook(@Argument BookInput bookInput) { // ... @SubscriptionMapping public Flux<Book> newPublications() { // ...
@SchemaMapping
handler methods have flexible signatures and can choose from a range of method arguments and return values..
5.2.1. Method Signature
Schema mapping handler methods can have any of the following method arguments:
@Argument
For access to a named field argument bound to a higher-level, typed Object.
@Argument Map<String, Object>
For access to the raw argument value.
See
@Argument
.
ArgumentValue
For access to a named field argument bound to a higher-level, typed Object along with a flag to indicate if the input argument was omitted vs set to
null
.See
ArgumentValue
.
@Arguments
For access to all field arguments bound to a higher-level, typed Object.
See
@Arguments
.
@Arguments Map<String, Object>
For access to the raw map of arguments.
@ProjectedPayload
InterfaceFor access to field arguments through a project interface.
See
@ProjectedPayload
Interface."Source"
For access to the source (i.e. parent/container) instance of the field.
See Source.
Subrange
andScrollSubrange
For access to pagination arguments.
See Pagination, Scroll,
Subrange
.For access to sort details.
See Pagination,
Sort
.
DataLoader
For access to a
DataLoader
in theDataLoaderRegistry
.See
DataLoader
.
@ContextValue
For access to an attribute from the main
GraphQLContext
inDataFetchingEnvironment
.
@LocalContextValue
For access to an attribute from the local
GraphQLContext
inDataFetchingEnvironment
.
GraphQLContext
For access to the context from the
DataFetchingEnvironment
.
java.security.Principal
Obtained from the Spring Security context, if available.
@AuthenticationPrincipal
For access to
Authentication#getPrincipal()
from the Spring Security context.
DataFetchingFieldSelectionSet
For access to the selection set for the query through the
DataFetchingEnvironment
.
Locale
,Optional<Locale>
For access to the
Locale
from theDataFetchingEnvironment
.
DataFetchingEnvironment
For direct access to the underlying
DataFetchingEnvironment
.
Mono
andFlux
for asynchronous value(s). Supported for controller methods and for anyDataFetcher
as described in ReactiveDataFetcher
.
java.util.concurrent.Callable
to have the value(s) produced asynchronously. For this to work,AnnotatedControllerConfigurer
must be configured with anExecutor
.5.2.2.
@Argument
In GraphQL Java,
DataFetchingEnvironment
provides access to a map of field-specific argument values. The values can be simple scalar values (e.g. String, Long), aMap
of values for more complex input, or aList
of values.Use the
@Argument
annotation to have an argument bound to a target object and injected into the handler method. Binding is performed by mapping argument values to a primary data constructor of the expected method parameter type, or by using a default constructor to create the object and then map argument values to its properties. This is repeated recursively, using all nested argument values and creating nested target objects accordingly. For example:@Controller public class BookController { @QueryMapping public Book bookById(@Argument Long id) { // ... @MutationMapping public Book addBook(@Argument BookInput bookInput) { // ... If the target object doesn’t have setters, and you can’t change that, you can use a property on
AnnotatedControllerConfigurer
to allow falling back on binding via direct field access.By default, if the method parameter name is available (requires the
The-parameters
compiler flag with Java 8+ or debugging info from the compiler), it is used to look up the argument. If needed, you can customize the name through the annotation, e.g.@Argument("bookInput")
.@Argument
annotation does not have a "required" flag, nor the option to specify a default value. Both of these can be specified at the GraphQL schema level and are enforced by GraphQL Java.If binding fails, a
BindException
is raised with binding issues accumulated as field errors where thefield
of each error is the argument path where the issue occurred.You can use
@Argument
with aMap<String, Object>
argument, to obtain the raw value of the argument. For example:
@Controller public class BookController { @MutationMapping public Book addBook(@Argument Map<String, Object> bookInput) { // ... Prior to 1.2,
@Argument Map<String, Object>
returned the full arguments map if the annotation did not specify a name. After 1.2,@Argument
withMap<String, Object>
always returns the raw argument value, matching either to the name specified in the annotation, or to the parameter name. For access to the full arguments map, please use
@Arguments
instead.5.2.3.
ArgumentValue
By default, input arguments in GraphQL are nullable and optional, which means an argument can be set to the
null
literal, or not provided at all. This distinction is useful for partial updates with a mutation where the underlying data may also be, either set tonull
or not changed at all accordingly. When using@Argument
there is no way to make such a distinction, because you would getnull
or an emptyOptional
in both cases.If you want to know not whether a value was not provided at all, you can declare an
ArgumentValue
method parameter, which is a simple container for the resulting value, along with a flag to indicate whether the input argument was omitted altogether. You can use this instead of@Argument
, in which case the argument name is determined from the method parameter name, or together with@Argument
to specify the argument name.For example:
@Controller public class BookController { @MutationMapping public void addBook(ArgumentValue<BookInput> bookInput) { if (!bookInput.isOmitted()) { BookInput value = bookInput.value(); // ...
ArgumentValue
is also supported as a field within the object structure of an@Argument
method parameter, either initialized via a constructor argument or via a setter, including as a field of an object nested at any level below the top level object.5.2.4.
@Arguments
Use the
@Arguments
annotation, if you want to bind the full arguments map onto a single target Object, in contrast to@Argument
, which binds a specific, named argument.For example,
@Argument BookInput bookInput
uses the value of the argument "bookInput" to initializeBookInput
, while@Arguments
uses the full arguments map and in that case, top-level arguments are bound toBookInput
properties.You can use
@Arguments
with aMap<String, Object>
argument, to obtain the raw map of all argument values.5.2.5.
@ProjectedPayload
InterfaceAs an alternative to using complete Objects with
@Argument
, you can also use a projection interface to access GraphQL request arguments through a well-defined, minimal interface. Argument projections are provided by Spring Data’s Interface projections when Spring Data is on the class path.To make use of this, create an interface annotated with
@ProjectedPayload
and declare it as a controller method parameter. If the parameter is annotated with@Argument
, it applies to an individual argument within theDataFetchingEnvironment.getArguments()
map. When declared without@Argument
, the projection works on top-level arguments in the complete arguments map.For example:
@Controller public class BookController { @QueryMapping public Book bookById(BookIdProjection bookId) { // ... @MutationMapping public Book addBook(@Argument BookInputProjection bookInput) { // ... @ProjectedPayload interface BookIdProjection { Long getId(); @ProjectedPayload interface BookInputProjection { String getName(); @Value("#{target.author + ' ' + target.name}") String getAuthorAndName();
5.2.6. Source
In GraphQL Java, the
DataFetchingEnvironment
provides access to the source (i.e. parent/container) instance of the field. To access this, simply declare a method parameter of the expected target type.@Controller public class BookController { @SchemaMapping public Author author(Book book) { // ...
The source method argument also helps to determine the type name for the mapping. If the simple name of the Java class matches the GraphQL type, then there is no need to explicitly specify the type name in the
@SchemaMapping
annotation.
When there is a
CursorStrategy
bean in Spring configuration, controller methods support aSubrange<P>
argument where<P>
is a relative position converted from a cursor. For Spring Data,ScrollSubrange
exposesScrollPosition
. For example:@Controller public class BookController { @QueryMapping public Window<Book> books(ScrollSubrange subrange) { ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial()) int count = subrange.count().orElse(20); // ... @QueryMapping public Window<Book> books(Optional<Sort> optionalSort) { Sort sort = optionalSort.orElse(Sort.by(..));
5.2.9.
DataLoader
When you register a batch loading function for an entity, as explained in Batch Loading, you can access the
DataLoader
for the entity by declaring a method argument of typeDataLoader
and use it to load the entity:@Controller public class BookController { public BookController(BatchLoaderRegistry registry) { registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { // return Map<Long, Author> @SchemaMapping public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) { return loader.load(book.getAuthorId());
By default,
BatchLoaderRegistry
uses the full class name of the value type (e.g. the class name forAuthor
) for the key of the registration, and therefore simply declaring theDataLoader
method argument with generic types provides enough information to locate it in theDataLoaderRegistry
. As a fallback, theDataLoader
method argument resolver will also try the method argument name as the key but typically that should not be necessary.Note that for many cases with loading related entities, where the
@SchemaMapping
simply delegates to aDataLoader
, you can reduce boilerplate by using a @BatchMapping method as described in the next section.5.2.10. Validation
When a
javax.validation.Validator
bean is found,AnnotatedControllerConfigurer
enables support for Bean Validation on annotated controller methods. Typically, the bean is of typeLocalValidatorFactoryBean
.Bean validation lets you declare constraints on types:
public class BookInput { @NotNull private String title; @NotNull @Size(max=13) private String isbn;
If an error occurs during validation, a
ConstraintViolationException
is raised. You can use the Exceptions chain to decide how to present that to clients by turning it into an error to include in the GraphQL response.Validation and Kotlin CoroutinesHibernate Validator is not compatible with Kotlin Coroutine methods and fails when introspecting their method parameters. Please see spring-projects/spring-graphql#344 (comment) for links to relevant issues and a suggested workaround.
Batch Loading addresses the N+1 select problem through the use of an
org.dataloader.DataLoader
to defer the loading of individual entity instances, so they can be loaded together. For example:@Controller public class BookController { public BookController(BatchLoaderRegistry registry) { registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { // return Map<Long, Author> @SchemaMapping public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) { return loader.load(book.getAuthorId());
For the straight-forward case of loading an associated entity, shown above, the
@SchemaMapping
method does nothing more than delegate to theDataLoader
. This is boilerplate that can be avoided with a@BatchMapping
method. For example:@Controller public class BookController { @BatchMapping public Mono<Map<Book, Author>> author(List<Book> books) { // ...
The above becomes a batch loading function in the
BatchLoaderRegistry
where keys areBook
instances and the loaded values their authors. In addition, aDataFetcher
is also transparently bound to theauthor
field of the typeBook
, which simply delegates to theDataLoader
for authors, given its source/parentBook
instance.By default, the field name defaults to the method name, while the type name defaults to the simple class name of the input
List
element type. Both can be customized through annotation attributes. The type name can also be inherited from a class level@SchemaMapping
.5.3.1. Method Signature
Batch mapping methods support the following arguments:
@ContextValue
For access to a value from the
GraphQLContext
ofBatchLoaderEnvironment
, which is the same context as the one from theDataFetchingEnvironment
.
GraphQLContext
For access to the context from the
BatchLoaderEnvironment
, which is the same context as the one from theDataFetchingEnvironment
.
BatchLoaderEnvironment
The environment that is available in GraphQL Java to a
org.dataloader.BatchLoaderWithContext
.
Flux<V>
A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method.
Map<K,V>
,Collection<V>
Imperative variants, e.g. without remote calls to make.
Callable<Map<K,V>>
,Callable<Collection<V>>
Imperative variants to be invoked asynchronously. For this to work,
AnnotatedControllerConfigurer
must be configured with anExecutor
.5.4.
@GraphQlExceptionHandler
Use
@GraphQlExceptionHandler
methods to handle exceptions from data fetching with a flexible method signature. When declared in a controller, exception handler methods apply to exceptions from the same controller:@Controller public class BookController { @QueryMapping public Book bookById(@Argument Long id) { // ... @GraphQlExceptionHandler public GraphQLError handle(BindException ex) { return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); @GraphQlExceptionHandler public GraphQLError handle(BindException ex) { return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
Exception handling via
@GraphQlExceptionHandler
methods is applied automatically to controller invocations. To handle exceptions from othergraphql.schema.DataFetcher
implementations, not based on controller methods, obtain aDataFetcherExceptionResolver
fromAnnotatedControllerConfigurer
, and register it inGraphQlSource.Builder
as a DataFetcherExceptionResolver.5.4.1. Method Signature
Exception handler methods support a flexible method signature with method arguments resolved from a
DataFetchingEnvironment,
and matching to those of @SchemaMapping methods.Supported return types are listed below:
Object
Resolve the exception to a single error, to multiple errors, or none. The return value must be
GraphQLError
,Collection<GraphQLError>
, ornull
.
Mono<T>
For asynchronous resolution where
<T>
is one of the supported, synchronous, return types.The path to a Web GraphQL endpoint can be secured with HTTP URL security to ensure that only authenticated users can access it. This does not, however, differentiate among different GraphQL requests on such a shared endpoint on a single URL.
To apply more fine-grained security, add Spring Security annotations such as
@PreAuthorize
or@Secured
to service methods involved in fetching specific parts of the GraphQL response. This should work due to Context Propagation that aims to make Security, and other context, available at the data fetching level.The 1.0.x branch of this repository contains samples for Spring MVC and for WebFlux.
Observability support with Micrometer is directly instrumented in Spring for GraphQL. This enables both metrics and traces for GraphQL requests and "non-trivial" data fetching operations. Because the GraphQL engine operates on top of a transport layer, you should also expect observations from the transport, if supported in Spring Framework.
Observations are only published if an
ObservationRegistry
is configured in the application. You can learn more about configuring the observability infrastructure in Spring Boot. If you would like to customize the metadata produced with the GraphQL observations, you can configure a custom convention on the instrumentation directly. If your application is using Spring Boot, contributing the custom convention as a bean is the preferred way.7.1. Server Requests instrumentation
GraphQL Server Requests observations are created with the name
"graphql.request"
for traditional and Reactive applications and above all supported transports. This instrumentation assumes that any parent observation must be set as the current one on the GraphQL context with the well-known"micrometer.observation"
key. For trace propagation across network boundaries, a separate instrumentation at the transport level must be in charge. In the case of HTTP, Spring Framework has dedicated instrumentation that takes care of trace propagation.Applications need to configure the
org.springframework.graphql.observation.GraphQlObservationInstrumentation
instrumentation in their application. It is using theorg.springframework.graphql.observation.DefaultExecutionRequestObservationConvention
by default, backed by theExecutionRequestObservationContext
.By default, the following KeyValues are created:
Table 1. Low cardinality KeysThe
Table 2. High cardinality Keysgraphql.operation
KeyValue will use the custom name of the provided query, or the standard name for the operation if none ("query"
,"mutation"
or"subscription"
). Thegraphql.outcome
KeyValue will be"SUCCESS"
if a valid GraphQL response has been sent,"REQUEST_ERROR"
if the request could not be parsed, or"INTERNAL_ERROR"
if no valid GraphQL response could be produced.7.2. DataFetcher instrumentation
GraphQL DataFetcher observations are created with the name
"graphql.datafetcher"
, only for data fetching operations that are considered as "non trivial" (property fetching on a Java object is a trivial operation). Applications need to configure theorg.springframework.graphql.observation.GraphQlObservationInstrumentation
instrumentation in their application. It is using theorg.springframework.graphql.observation.DefaultDataFetcherObservationConvention
by default, backed by theDataFetcherObservationContext
.By default, the following KeyValues are created:
Table 3. Low cardinality KeysSpring Framework 6.0 introduced the support infrastructure for compiling Spring applications to GraalVM Native images. If you are not familiar with GraalVM in general, how this differs from applications deployed on the JVM and what it means for Spring application, please refer to the dedicated Spring Boot 3.0 GraalVM Native Image support documentation. Spring Boot also documents the know limitations with the GraalVM support in Spring.
8.1. GraphQL Java metadata
Since the static analysis of your application is done at build time, GraalVM might need extra hints if your application is looking up static resources, performing reflection or creating JDK proxies at runtime.
GraphQL Java is performing three tasks at runtime that Native Images are sensible to:
The first two items are handled via reachability metadata that has been contributed by the Spring team to the GraalVM reachability metadata repository. This metadata is automatically fetched by the native compilation tool when building an application that depends on GraphQL Java. This doesn’t cover our third item in the list, as those types are provided by the application itself and must be discovered by another mean.
8.2. Native Server applications support
In typical Spring for GraphQL applications, Java types tied to the GraphQL schema are exposed in
@Controller
method signatures as parameters or return types. During the Ahead Of Time processing phase of the build, Spring or GraphQL will use itso.s.g.data.method.annotation.support.SchemaMappingBeanFactoryInitializationAotProcessor
to discover the relevant types and register reachability metadata accordingly. This is all done automatically for you if you are building a Spring Boot application with GraalVM support.If your application is "manually" registering data fetchers, some types are not discoverable as a result. You should then register them with Spring Framework’s
@RegisterReflectionForBinding
:import graphql.schema.DataFetcher; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.graphql.data.query.QuerydslDataFetcher; import org.springframework.graphql.execution.RuntimeWiringConfigurer; @Configuration @RegisterReflectionForBinding(Book.class) (3) public class GraphQlConfiguration { @Bean RuntimeWiringConfigurer customWiringConfigurer(BookRepository bookRepository) { (1) DataFetcher<Book> dataFetcher = QuerydslDataFetcher.builder(bookRepository).single(); return wiringBuilder -> wiringBuilder .type("Query", builder -> builder.dataFetcher("book", dataFetcher)); (2)
8.3. Client support
The
GraphQlClient
is not necessarily present as a bean in the application context and it does not expose the Java types used in the schema in method signatures. TheAotProcessor
strategy described in the section above cannot be used as a result. For client support, Spring for GraphQL embeds the relevant reachability metadata for the client infrastructure. When it comes to Java types used by the application, applications should use a similar strategy as "manual" data fetchers using@RegisterReflectionForBinding
:import reactor.core.publisher.Mono; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.graphql.client.GraphQlClient; import org.springframework.stereotype.Component; @Component @RegisterReflectionForBinding(Project.class) (2) public class ProjectService { private final GraphQlClient graphQlClient; public ProjectService(GraphQlClient graphQlClient) { this.graphQlClient = graphQlClient; public Mono<Project> project(String projectSlug) { String document = """ query projectWithReleases($projectSlug: ID!) { project(slug: $projectSlug) { releases { version return this.graphQlClient.document(document) .variable("projectSlug", projectSlug) .retrieve("project") .toEntity(Project.class); (1) In a Native image, we need to ensure that reflection can be performed on
Project
at runtime@RegisterReflectionForBinding
will register the relevant hints for theProject
type and all types exposed as fieldsSpring for GraphQL includes client support for executing GraphQL requests over HTTP, WebSocket, and RSocket.
9.1.
GraphQlClient
GraphQlClient
is a contract that declares a common workflow for GraphQL requests that is independent of the underlying transport. That means requests are executed with the same API no matter what the underlying transport, and anything transport specific is configured at build time.To create a
GraphQlClient
you need one of the following extensions:Each defines a
Builder
with options relevant to the transport. All builders extend from a common, base GraphQlClientBuilder
with options relevant to all extensions.Once you have a
GraphQlClient
you can begin to make requests.9.1.1. HTTP
HttpGraphQlClient
uses WebClient to execute GraphQL requests over HTTP.WebClient webClient = ... ; HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
Once
HttpGraphQlClient
is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, usemutate()
on an existingHttpGraphQlClient
to create a new instance with customized settings:WebClient webClient = ... ; HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Perform requests with graphQlClient... HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Perform requests with anotherGraphQlClient...
9.1.2. WebSocket
WebSocketGraphQlClient
executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:String url = "wss://localhost:8080/graphql"; WebSocketClient client = new ReactorNettyWebSocketClient(); WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
In contrast to
HttpGraphQlClient
, theWebSocketGraphQlClient
is connection oriented, which means it needs to establish a connection before making any requests. As you begin to make requests, the connection is established transparently. Alternatively, use the client’sstart()
method to establish the connection explicitly before any requests.In addition to being connection-oriented,
Use a singleWebSocketGraphQlClient
is also multiplexed. It maintains a single, shared connection for all requests. If the connection is lost, it is re-established on the next request or ifstart()
is called again. You can also use the client’sstop()
method which cancels in-progress requests, closes the connection, and rejects new requests.WebSocketGraphQlClient
instance for each server in order to have a single, shared connection for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server.Once
WebSocketGraphQlClient
is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, usemutate()
on an existingWebSocketGraphQlClient
to create a new instance with customized settings:URI url = ... ; WebSocketClient client = ... ; WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use graphQlClient... WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Use anotherGraphQlClient...
The GraphQL over WebSocket protocol defines a number of connection oriented messages in addition to executing requests. For example, a client sends
"connection_init"
and the server responds with"connection_ack"
at the start of a connection.For WebSocket transport specific interception, you can create a
WebSocketGraphQlClientInterceptor
:static class MyInterceptor implements WebSocketGraphQlClientInterceptor { @Override public Mono<Object> connectionInitPayload() { // ... the "connection_init" payload to send @Override public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) { // ... the "connection_ack" payload received
Register the above interceptor as any other
GraphQlClientInterceptor
and use it also to intercept GraphQL requests, but note there can be at most one interceptor of typeWebSocketGraphQlClientInterceptor
.URI uri = URI.create("wss://localhost:8080/rsocket"); WebsocketClientTransport transport = WebsocketClientTransport.create(url); RSocketGraphQlClient client = RSocketGraphQlClient.builder() .clientTransport(transport) .build();
In contrast to
HttpGraphQlClient
, theRSocketGraphQlClient
is connection oriented, which means it needs to establish a session before making any requests. As you begin to make requests, the session is established transparently. Alternatively, use the client’sstart()
method to establish the session explicitly before any requests.Use a single
RSocketGraphQlClient
is also multiplexed. It maintains a single, shared session for all requests. If the session is lost, it is re-established on the next request or ifstart()
is called again. You can also use the client’sstop()
method which cancels in-progress requests, closes the session, and rejects new requests.RSocketGraphQlClient
instance for each server in order to have a single, shared session for all requests to that server. Each client instance establishes its own connection and that is typically not the intent for a single server.Once
RSocketGraphQlClient
is created, you can begin to execute requests using the same API, independent of the underlying transport.9.1.4. Builder
GraphQlClient
defines a parentBuilder
with common configuration options for the builders of all extensions. Currently, it has lets you configure:Once you have a
GraphQlClient
, you can begin to perform requests via retrieve() or execute() where the former is only a shortcut for the latter.9.2.1. Retrieve
The below retrieves and decodes the data for a query:
String document = "{" + " project(slug:\"spring-framework\") {" + " name" + " releases {" + " version" + " }"+ " }" + Mono<Project> projectMono = graphQlClient.document(document) (1) .retrieve("project") (2) .toEntity(Project.class); (3)
The input document is a
String
that could be a literal or produced through a code generated request object. You can also define documents in files and use a Document Source to resole them by file name.The path is relative to the "data" key and uses a simple dot (".") separated notation for nested fields with optional array indices for list elements, e.g.
"project.name"
or"project.releases[0].version"
.Decoding can result in
FieldAccessException
if the given path is not present, or the field value isnull
and has an error.FieldAccessException
provides access to the response and the field:Mono<Project> projectMono = graphQlClient.document(document) .retrieve("project") .toEntity(Project.class) .onErrorResume(FieldAccessException.class, ex -> { ClientGraphQlResponse response = ex.getResponse(); // ... ClientResponseField field = ex.getField(); // ...
Retrieve is only a shortcut to decode from a single path in the response map. For more control, use the
execute
method and handle the response:For example:
Mono<Project> projectMono = graphQlClient.document(document) .execute() .map(response -> { if (!response.isValid()) { // Request failure... (1) ClientResponseField field = response.field("project"); if (!field.hasValue()) { if (field.getError() != null) { // Field failure... (2) else { // Optional field set to null... (3) return field.toEntity(Project.class); (4)
9.2.3. Document Source
The document for a request is a
String
that may be defined in a local variable or constant, or it may be produced through a code generated request object.You can also create document files with extensions
.graphql
or.gql
under"graphql-documents/"
on the classpath and refer to them by file name.For example, given a file called
projectReleases.graphql
insrc/main/resources/graphql-documents
, with content:src/main/resources/graphql-documents/projectReleases.graphqlquery projectReleases($slug: ID!) { project(slug: $slug) { releases { version
You can then:
Mono<Project> projectMono = graphQlClient.documentName("projectReleases") (1) .variable("slug", "spring-framework") (2) .retrieve() .toEntity(Project.class);
9.3. Subscription Requests
GraphQlClient
can execute subscriptions over transports that support it. Only the WebSocket and RSocket transports support GraphQL subscriptions, so you’ll need to create a WebSocketGraphQlClient or RSocketGraphQlClient.9.3.1. Retrieve
To start a subscription stream, use
retrieveSubscription
which is similar to retrieve for a single response but returning a stream of responses, each decoded to some data:Flux<String> greetingFlux = client.document("subscription { greetings }") .retrieveSubscription("greeting") .toEntity(String.class);
The
Flux
may terminate withSubscriptionErrorException
if the subscription ends from the server side with an "error" message. The exception provides access to GraphQL errors decoded from the "error" message.The
Flux
may termiate withGraphQlTransportException
such asWebSocketDisconnectedException
if the underlying connection is closed or lost. In that case you can use theretry
operator to restart the subscription.To end the subscription from the client side, the
Flux
must be cancelled, and in turn the WebSocket transport sends a "complete" message to the server. How to cancel theFlux
depends on how it is used. Some operators such astake
ortimeout
themselves cancel theFlux
. If you subscribe to theFlux
with aSubscriber
, you can get a reference to theSubscription
and cancel through it. TheonSubscribe
operator also provides access to theSubscription
.9.3.2. Execute
Retrieve is only a shortcut to decode from a single path in each response map. For more control, use the
executeSubscription
method and handle each response directly:Flux<String> greetingFlux = client.document("subscription { greetings }") .executeSubscription() .map(response -> { if (!response.isValid()) { // Request failure... ClientResponseField field = response.field("project"); if (!field.hasValue()) { if (field.getError() != null) { // Field failure... else { // Optional field set to null... (3) return field.toEntity(String.class) @Override public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) { // ... return chain.next(request); @Override public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) { // ... return chain.next(request); WebSocketClient client = ... ; WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client) .interceptor(new MyInterceptor()) .build();
GraphiQL is a graphical interactive in-browser GraphQL IDE. It is very popular amongst developers as it makes it easy to explore and interactively develop GraphQL APIs. During development, a stock GraphiQL integration is often enough to help developers work on an API. In production, applications can require a custom GraphiQL build, that ships with a company logo or specific authentication support.
Spring for GraphQL ships with a stock GraphiQL
index.html
page that uses static resources hosted on the unpkg.com CDN. Spring Boot applications can easily enable this page with a configuration property.Your application may need a custom GraphiQL build if it requires a setup that doesn’t rely on a CDN, or if you wish to customize the user interface. This can be done in two steps:
10.1. Creating a custom GraphiQL build
This part is generally outside of the scope of this documentation, as there are several options for custom builds. You will find more information in the official GraphiQL documentation. You can choose to copy the build result directly in your application resources. Alternatively, you can integrate the JavaScript build in your project as a separate module by leveraging Node.js Gradle or Maven build plugins.
10.2. Exposing a GraphiQL instance
Once a GraphiQL build is available on the classpath, you can expose it as an endpoint with the functional web frameworks.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.graphql.server.webmvc.GraphiQlHandler; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.ServerResponse; @Configuration public class GraphiQlConfiguration { @Bean @Order(0) public RouterFunction<ServerResponse> graphiQlRouterFunction() { RouterFunctions.Builder builder = RouterFunctions.route(); ClassPathResource graphiQlPage = new ClassPathResource("graphiql/index.html"); (1) GraphiQlHandler graphiQLHandler = new GraphiQlHandler("/graphql", "", graphiQlPage); (2) builder = builder.GET("/graphiql", graphiQLHandler::handleRequest); (3) return builder.build(); (4) Load the GraphiQL page from the classpath (here, we are using the version shipped with Spring for GraphQL) Configure a web handler for processing HTTP requests; you can implement a custom
HandlerFunction
depending on your use case Finally, map the handler to a specific HTTP endpoint Expose this new route through aRouterFunction
beanSpring for GraphQL provides dedicated support for testing GraphQL requests over HTTP, WebSocket, and RSocket, as well as for testing directly against a server.
To make use of this, add
spring-graphql-test
to your build:Gradledependencies { // ... testImplementation 'org.springframework.graphql:spring-graphql-test:1.2.2' <dependency> <groupId>org.springframework.graphql</groupId> <artifactId>spring-graphql-test</artifactId> <version>1.2.2</version> <scope>test</scope> </dependency> </dependencies>
11.1.
GraphQlTester
GraphQlTester
is a contract that declares a common workflow for testing GraphQL requests that is independent of the underlying transport. That means requests are tested with the same API no matter what the underlying transport, and anything transport specific is configured at build time.To create a
GraphQlTester
that performs requests through a client, you need one of the following extensions:Each defines a
Builder
with options relevant to the transport. All builders extend from a common, base GraphQlTesterBuilder
with options relevant to all extensions.11.1.1. HTTP
HttpGraphQlTester
uses WebTestClient to execute GraphQL requests over HTTP, with or without a live server, depending on howWebTestClient
is configured.To test in Spring WebFlux, without a live server, point to your Spring configuration that declares the GraphQL HTTP endpoint:
ApplicationContext context = ... ; WebTestClient client = WebTestClient.bindToApplicationContext(context) .configureClient() .baseUrl("/graphql") .build(); HttpGraphQlTester tester = HttpGraphQlTester.create(client); WebTestClient client = MockMvcWebTestClient.bindToApplicationContext(context) .configureClient() .baseUrl("/graphql") .build(); HttpGraphQlTester tester = HttpGraphQlTester.create(client);
WebTestClient client = WebTestClient.bindToServer() .baseUrl("http://localhost:8080/graphql") .build(); HttpGraphQlTester tester = HttpGraphQlTester.create(client);
Once
HttpGraphQlTester
is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, usemutate()
on an existingHttpSocketGraphQlTester
to create a new instance with customized settings:HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use tester... HttpGraphQlTester anotherTester = tester.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Use anotherTester...
11.1.2. WebSocket
WebSocketGraphQlTester
executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:String url = "http://localhost:8080/graphql"; WebSocketClient client = new ReactorNettyWebSocketClient(); WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();
WebSocketGraphQlTester
is connection oriented and multiplexed. Each instance establishes its own single, shared connection for all requests. Typically, you’ll want to use a single instance only per server.Once
WebSocketGraphQlTester
is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, usemutate()
on an existingWebSocketGraphQlTester
to create a new instance with customized settings:URI url = ... ; WebSocketClient client = ... ; WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client) .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use tester... WebSocketGraphQlTester anotherTester = tester.mutate() .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Use anotherTester...
11.1.3. RSocket
RSocketGraphQlTester
usesRSocketRequester
from spring-messaging to execute GraphQL requests over RSocket:URI uri = URI.create("wss://localhost:8080/rsocket"); WebsocketClientTransport transport = WebsocketClientTransport.create(url); RSocketGraphQlTester client = RSocketGraphQlTester.builder() .clientTransport(transport) .build();
RSocketGraphQlTester
is connection oriented and multiplexed. Each instance establishes its own single, shared session for all requests. Typically, you’ll want to use a single instance only per server. You can use thestop()
method on the tester to close the session explicitly.
Once
RSocketGraphQlTester
is created, you can begin to execute requests using the same API, independent of the underlying transport.11.1.4.
GraphQlService
Many times it’s enough to test GraphQL requests on the server side, without the use of a client to send requests over a transport protocol. To test directly against a
ExecutionGraphQlService
, use theExecutionGraphQlServiceTester
extension:GraphQlService service = ... ; ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);
Once
ExecutionGraphQlServiceTester
is created, you can begin to execute requests using the same API, independent of the underlying transport.
ExecutionGraphQlServiceTester.Builder
provides an option to customizeExecutionInput
details:GraphQlService service = ... ; ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.builder(service) .configureExecutionInput((executionInput, builder) -> builder.executionId(id).build()) .build();
11.1.5.
WebGraphQlHandler
The
GraphQlService
extension lets you test on the server side, without a client. However, in some cases it’s useful to involve server side transport handling with given mock transport input.The
WebGraphQlTester
extension lets you processes request through theWebGraphQlInterceptor
chain before handing off toExecutionGraphQlService
for request execution:WebGraphQlHandler handler = ... ; WebGraphQlTester tester = WebGraphQlTester.create(handler);
WebGraphQlHandler handler = ... ; WebGraphQlTester tester = WebGraphQlTester.builder(handler) .headers(headers -> headers.setBasicAuth("joe", "...")) .build();
Once
WebGraphQlServiceTester
is created, you can begin to execute requests using the same API, independent of the underlying transport.11.1.6. Builder
GraphQlTester
defines a parentBuilder
with common configuration options for the builders of all extensions. It lets you configure the following:
errorFilter
- a predicate to suppress expected errors, so you can inspect the data of the response.
documentSource
- a strategy for loading the document for a request from a file on the classpath or from anywhere else.
responseTimeout
- how long to wait for request execution to complete before timing11.2. Requests
Once you have a
GraphQlTester
, you can begin to test requests. The below executes a query for a project and uses JsonPath to extract project release versions from the response:String document = "{" + " project(slug:\"spring-framework\") {" + " releases {" + " version" + " }"+ " }" + graphQlTester.document(document) .execute() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1);
You can also create document files with extensions
.graphql
or.gql
under"graphql-test/"
on the classpath and refer to them by file name.For example, given a file called
projectReleases.graphql
insrc/main/resources/graphql-test
, with content:query projectReleases($slug: ID!) { project(slug: $slug) { releases { version
You can then use:
graphQlTester.documentName("projectReleases") (1) .variable("slug", "spring-framework") (2) .execute() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1);
11.2.1. Nested Paths
By default, paths are relative to the "data" section of the GraphQL response. You can also nest down to a path, and inspect multiple paths relative to it as follows:
graphQlTester.document(document) .execute() .path("project", project -> project (1) .path("name").entity(String.class).isEqualTo("spring-framework") .path("releases[*].version").entityList(String.class).hasSizeGreaterThan(1));
11.3. Subscriptions
To test subscriptions, call
executeSubscription
instead ofexecute
to obtain a stream of responses and then useStepVerifier
from Project Reactor to inspect the stream:Flux<String> greetingFlux = tester.document("subscription { greetings }") .executeSubscription() .toFlux("greetings", String.class); // decode at JSONPath StepVerifier.create(greetingFlux) .expectNext("Hi") .expectNext("Bonjour") .expectNext("Hola") .verifyComplete();
11.4. Errors
When you use
verify()
, any errors under the "errors" key in the response will cause an assertion failure. To suppress a specific error, use the error filter beforeverify()
:graphQlTester.query(query) .execute() .errors() .filter(error -> ...) .verify() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1);
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client) .errorFilter(error -> ...) .build();
Spring Boot provides a starter for building GraphQL applications with Spring for GraphQL. For version information, see the Spring for GraphQL Versions wiki page.
The easiest way to get started is via start.spring.io by selecting "Spring for GraphQL" along with an underlying transport such as Spring MVC of WebFlux over HTTP or WebSocket, or over RSocket. Refer to the Spring for GraphQL Starter section in the Spring Boot reference for details on supported transports, auto-configuration related features, and more. For testing support, see Auto-Configured GraphQL Tests.
For further reference, check the following GraphQL related:
In addition, the 1.0.x branch of this repository contains sample applications for various scenarios. Those samples do not exist in the
main
branch and are planned to be moved out into a separate repository. To run those samples, check out the 1.0.x branch run their main application classes from your IDE, or from the command line:$ ./gradlew :samples:{sample-directory-name}:bootRun