Andreas Marek_ Donna Zhou - GraphQL With Java and Spring-Leanpub (2023)
Andreas Marek_ Donna Zhou - GraphQL With Java and Spring-Leanpub (2023)
Spring
Andreas Marek
Donna Zhou
GraphQL with Java and
Spring
GraphQL with Java and Spring
Prologue
Andi (Andreas)
Donna
About this book
Introduction
Your first Spring for GraphQL service
What is GraphQL?
A brief history of GraphQL
From GraphQL Java to Spring for GraphQL
Overview
Three layers
Schema and SDL
GraphQL query language
Request and Response
Execution and DataFetcher
How concepts relate to each other
GraphQL Java
Spring for GraphQL
Schema
Schema-first
Loading schema resources in Spring for GraphQL
GraphQL schema elements
GraphQL types
Fields everywhere
Scalar
Enum
Object
Input object
Interface
Union
List and NonNull
Directives
Arguments
Documentation with descriptions
Comments
GraphQL query language
Literals
Operations
Query operations
Mutation operations
Subscription operations
Arguments
Fragments
Inline fragments
Variables
Aliases
GraphQL document
Named and unnamed operations
Query language in GraphQL Java
DataFetchers
Spring for GraphQL annotated methods
PropertyDataFetchers in Spring for GraphQL
DataFetchers and schema mapping handler methods
TypeResolver in Spring for GraphQL
Arguments in Spring for GraphQL
More Spring for GraphQL inputs
Adding custom scalars in Spring for GraphQL
Under the hood: DataFetchers inside GraphQL Java
DataFetchers in GraphQL Java
Source objects in GraphQL Java
RuntimeWiring in GraphQL Java
Creating an executable schema in GraphQL Java
TypeResolver in GraphQL Java
Building a GraphQL service
Spring for GraphQL
GraphQL Java
Spring WebFlux or Spring MVC
Reading schemas
Configuration properties
Expanding our Spring for GraphQL service
Pet schema
Controllers
Fetching data from an external service
Source object
GraphQL arguments
Mutations
Unions, interfaces, and TypeResolver
Subscriptions
Getting started
Execution
Protocol
Client support
Request and response
Transport protocols and serialization
Request
Response
HTTP status codes
HTTP headers
Intercepting requests
GraphQL errors
Request errors
Field errors
How errors appear in the response
Error classifications
How to return errors
Throw exception during DataFetcher invocation
Customizing exception resolution
Return data and errors with DataFetcherResult
Schema design
Schema-first and implementation-agnostic
Evolution over versioning
Connected
Schema elements are cheap
Nullable fields
Nullable input fields and arguments
Pagination for lists with Relay’s cursor connection specification
Relay’s cursor connections specification
Schema
Query and response
Requesting more pages
Key concepts of Relay’s cursor connections specification
Expected errors
Mutation format
Naming standards
DataFetchers in depth
More DataFetcher inputs
Global context
Local context
DataFetcher implementation patterns
Spring for GraphQL Reactor support
Directives
Schema and operation directives
Built-in directives
@skip and @include
@deprecated
@specifiedBy
Defining your own schema and operation directives
Defining schema directives
Defining operation directives
Repeatable directives
Implementing logic for schema directives
Changing execution logic with schema directives
Validation with schema directives
Adding metadata with schema directives
Implementing logic for operation directives
Execution
Initializing execution objects
How Spring for GraphQL starts execution
Execution steps
Parsing and validation
Coercing variables
Fetching data
Reactive concurrency-agnostic
Completing a field
TypeResolver
Query vs mutation
Instrumentation
Instrumentation in Spring for GraphQL
Writing a custom instrumentation
InstrumentationContext
InstrumentationState
ChainedInstrumentation
Built-in instrumentations
List of instrumentation hooks
DataLoader
The n+1 problem
Solving the n+1 problem
DataLoader overview
DataLoader and GraphQL Java
DataLoader and Spring for GraphQL
@BatchMapping method signature
Testing
Unit testing DataFetcher
GraphQlTester
document or documentName
GraphQlTester.Request and execute
GraphQlTester.Response, path, entity, entityList
errors
Testing different layers
End-to-end over HTTP
Application test
WebGraphQlHandler test
ExecutionGraphQlService test
Focused GraphQL testing with @GraphQlTest
Subscription testing
Testing recommendations
Security
Securing a Spring for GraphQL service
Spring for GraphQL support for security
Method security
Testing auth
Java client
HTTP client
WebSocket client
GraphQlClient
GraphQL with Java and
Spring
Prologue
Andi (Andreas)
In 2015, I (Andi) was working as a software developer for a small
company in Berlin, Germany. During a conversation, one of my
colleagues (thanks a lot Stephan!) mentioned to me this new
technology called “GraphQL”, aimed at improving the way clients
access data from a service, and they planned to release it soon.
Many thanks and a special mention belongs to Brad Baker, who has
been a co-maintainer for over six years. There is no way to overstate
his contributions and influence on GraphQL Java. It is as much his
project as it is mine.
Most importantly I want to thank my wife Elli for all her support:
without her there would be no book today.
Donna
I (Donna) am thrilled to write this book with Andi, who created
GraphQL Java and played a major role in the creation of Spring for
GraphQL. Andi, Brad, and I are the maintainers of GraphQL Java.
All code examples were written with Java 17, which is the minimum
version required for Spring Boot 3.x. Examples in this book were
written with Spring Boot 3.0.4 and Spring for GraphQL 1.1.2, which
uses GraphQL Java 19.2.
Spring Start
Let’s make this service more useful and implement a very simple
GraphQL service, which serves pet data. To keep this initial example
simple, the data will be an in-memory list. Later, in the Building a
GraphQL service chapter, we’ll extend this example to call another
service.
For this initial example, we’ll cover concepts at a high level, so we
can quickly arrive at a working service you can interact with. In the
coming chapters, we will explain these concepts in greater detail.
type Query {
pets: [Pet]
}
type Pet {
name: String
color: String
}
Next, we’ll add the logic to connect our schema with the pet data.
Create a new Java class PetsController in the package
myservice.service.
@Controller
class PetsController {
@QueryMapping
List< Pet> pets() {
return List.of(
new Pet( "Luna", "cappuccino"),
new Pet( "Skipper", "black"));
}
And that’s all the code we need: a schema file, a record class, and a
GraphQL controller! The @QueryMapping controller annotation
registers the pets method as a DataFetcher, connecting the pets field
in the schema to data, in this case an in-memory list. We’ll explain
how to connect your schema and your data in much more detail in
the DataFetchers chapter.
Restart your service. You should see a log entry GraphQL endpoint
HTTP POST /graphql if you’re successful. Then open GraphiQL by
navigating to http://localhost:8080/graphiql.
Let’s try our first query. Enter the following on the left-hand side of
the GraphiQL playground:
query myPets {
pets {
name
color
}
}
Then send the query by clicking the play button, a pink button with
a triangle icon, as in the screenshot “GraphiQL request and
response”.
Then click on the root Query for pets, and then click on Pet to see its
attributes (name and color), as shown in the screenshot “GraphiQL
Pet documentation”.
What is GraphQL?
GraphQL is a technology for client-server data exchange. The typical
uses cases are web or mobile clients accessing or changing data on a
backend server, as shown in the diagram “Client Server”. We
sometimes describe the two parties involved as the API “consumer”
and “producer”.
Client Server
query myPets {
pets {
name
}
}
For example, the API used by the queries above would look like this:
# This is a comment
As you can see the special field __schema starts with __, which
indicates this is an introspection field, not a normal field.
After the open source release in 2015, GraphQL was owned and run
by Facebook until the end of 2018 with the creation of the GraphQL
Foundation. The GraphQL Foundation is a vendor-neutral entity,
comprising over 25 members. The list includes AWS, Airbnb,
Atlassian, Microsoft, IBM, and Shopify. The official description of
the foundation is:
The most important group for developing the GraphQL spec is the
GraphQL Working Group (often shortened to WG). It is an open
group that meets online three times a month and mainly discusses
GraphQL spec changes and improvements. Everybody from the
GraphQL community can join. More details are available in the
working group GitHub repository.
From GraphQL Java to Spring for
GraphQL
Shortly after GraphQL was open sourced, a first version of GraphQL
Java was released. One of the fundamental design decisions that I
(Andi) made was to focus purely on the execution part of GraphQL.
GraphQL Java always aimed to be a spec-compliant GraphQL
engine, not a fully-fledged framework for GraphQL services. This
meant that GraphQL Java should never deal with HTTP I/O or any
kind of threading, to the extent that is possible.
So some time after I released GraphQL Java, the first GraphQL Java
Spring integrations became available. I even developed a small
GraphQL Java Spring library, which aimed to be as lightweight as
possible. But nothing beats an official Spring integration maintained
by the Spring team that allows for the most GraphQL adoption and
best experience overall.
In July 2020, the Spring and GraphQL Java teams came together to
develop an official Spring for GraphQL integration. One year later,
we published a first milestone and after that, the first release of
Spring for GraphQL in May 2022.
In the next few chapters, we will explain the concepts used in this
initial service in greater detail, and also discuss core GraphQL
concepts. After that, we will build a more substantial application to
review what we have learned. Later in the book, we’ll cover more
advanced topics.
Overview
In this chapter, we cover the fundamentals aspects of Spring for
GraphQL and GraphQL Java, and how they relate to each other.
This is important to build an overall understanding and not get lost
in the details in the next chapters.
Three layers
We have three layers to consider, where the higher ones depend on
the lower ones.
Three layers
We can consider GraphQL Java and the spec as the same from a
practical Java point of view. GraphQL Java doesn’t offer major
features beyond the spec and the spec doesn’t define anything that
is not represented in Java, it is a one-to-one relationship. Therefore,
we will not discuss the spec separately from GraphQL Java, and we
can assume features in GraphQL Java by default to be defined in the
spec.
type Pet {
name: String
color: String
}
This is a schema in SDL format defining two types: Query and Pet.
The Query type has a pets field. The Pet type has two fields, name and
color.
The SDL format is great for defining a schema, and makes the
schema easily readable. During execution, GraphQL Java uses an
instance of GraphQLSchema, which represents the provided SDL
schema.
A response can also contain another two top level keys, “errors” and
“extensions”. We’ll discuss this in more detail in the Request and
Response chapter.
The response can contain data, errors, and extensions. Data can be
anything. In ExecutionResult, data is Map<String, Object>. On the
transport layer, we send the response over HTTP in JSON.
The first step is purely Spring and can also involve aspects like
authentication. Then Spring invokes GraphQL Java and once
finished, the response is again handled by Spring and sent back to
the client.
@Controller
class PetsController {
@QueryMapping
List< Pet> pets() {
return List.of(
new Pet( "Luna", "cappuccino"),
new Pet( "Skipper", "black"));
}
For more on execution, see the dedicated chapter later in this book.
How concepts relate to each other
Concept relations
GraphQL Java
The primary classes of GraphQL Java are, as shown in the diagram
“GraphQL Java classes”:
Schema-first
“Schema-first” refers to the idea that the design of a GraphQL
schema should be done on its own, and should not be generated or
inferred from something else. The schema should not be generated
from a database schema, Java domain classes, nor a REST API.
GraphQL types
The most important schema elements are the types. There are eight
types in the GraphQL type system: Object, Interface, Union, Enum,
Scalar, InputObject, List, and NonNull. The first six are “named
types”, because each type has a unique name across the whole
schema, while List and NonNull are called “wrapping types”,
because they wrap named types, as we will see later.
Fields everywhere
The most prominent elements of a schema are fields. Objects and
interfaces contain fields which can have arguments. An input object
has input fields, which cannot have arguments. If we squint, we can
think of a schema as a list of types with a list of fields.
Scalar
A scalar is a primitive type describing a certain set of allowed values.
For example, a Boolean scalar means the value can be true or false,
an Int can be any number between -2^31 and 2^31, and a String can
be any String literal. A scalar name must be unique across the
schema.
GraphQL comes with five built-in scalars: String, Int, Float, Boolean,
and ID. In addition, every GraphQL service can define its own
custom scalars.
type Pet {
"String is a built-in scalar, therefore no declaration is requir
name: String
dateOfBirth: Date
}
The built-in @specifiedBy directive links to a custom scalar
specification URL. We’ll discuss this in more detail in the Directives
chapter. The @specifiedBy directive is optional, but is highly
recommended.
Enum
An enum type describes a list of possible values. It can be used as an
input or output type. Enums and scalars are the primitive types of
the GraphQL type system. An enum name must be unique across
the schema.
type Pet {
name: String
kind: PetKind # used as output type
}
type Query {
pets(kind: PetKind!): [Pet] # used as input type
}
Object
A GraphQL object type describes a certain shape of data as a list of
fields. It has a unique name across the schema. Each field has a
specific type, which must be an output type. Every field has an
optional list of arguments. Recursive references are allowed.
type Person {
name: String
}
type Query {
pet(name: String!): Pet # lookup a pet via name
}
Input object
An input object type describes a group of input fields where each
has an input type. An input object name must be unique across the
schema.
In SDL, an input object is declared via input.
input PetFilter {
minAge: Int
maxAge: Int
}
type Pet {
name: String
age: Int
}
type Query {
pets(filter: PetFilter): [Pets]
}
Interface
Similar to an object, an interface describes a certain shape of data as
a list of fields and has a name. In contrast to an object, an interface
can be implemented by another interface or object. An interface or
object implementing an interface must contain at least the same
fields defined in the interface. In that sense, an interface is used to
describe an abstract shape, which can be realized by different
objects.
# Another implementation
type Cat implements Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
doesMeow: Boolean # additional field specific to Cat
}
Union
A union type must be one of the member types at execution time. In
other words, a union is fully described by the list of possible object
types it can be at execution time.
type Dog {
name: String
doesBark: Boolean
}
type Cat {
name: String
doesMeow: Boolean
}
A list type is a list of the wrapped type. A non-null type marks this
type as never being null.
type Pet {
id: ID!
ownerNames: [String!] # A combination: a list of non-null string
}
GraphQL Java ensures that any field or input field marked as non-
null is never null. In GraphQL Java, list types are represented by
instances of GraphQLList and non-null types by instances of
GraphQLNonNull.
Directives
A directive is a schema element that allows us to define metadata
for a schema or a GraphQL operation. A directive needs to declare
all possible locations where it can be used. A directive contains an
optional list of arguments, similarly to fields.
# Example usage
type SomeType {
field(arg: Int @example): String @example
}
Arguments
Directives, object type fields, and interface fields can have an
optional list of arguments.
Every argument has a name and type, which must be an input type.
In the following example, the pet field has one defined argument
called name which is of type String!.
type Query {
pet(name: String!): Pet # lookup a pet via name
}
type Pet {
name: String
color: String
}
"""
A Pet can be a Dog or or Cat.
A Pet has a human owner.
"""
type Pet {
name: String
owner: Person
}
Documentation in GraphiQL
Comments
Comments start with a hash sign # and everything on the same line
is considered a part of a comment. For example:
type Query {
# This is a comment
hello: String
}
Literals
The query language contains several literals that mirror the schema
input types.
Operations
There are three operations in GraphQL:
Query operations
A query operation requests data and returns a response containing
the result.
type Pet {
name: String
}
type Pet {
name: String
}
Mutation operations
A mutation operation is a write followed by a fetch. Mutations
should have a side effect, which usually means changing (or
“mutating”) some data.
type Mutation {
changeUser(newName: String!): User
}
type User {
name: String
address: Address
}
type Address {
street: String
country: String
}
After changing the user’s name to “Brad”, we can query the details
of the changed user as a normal query. Notice how the sub-selection
looks exactly like a query sub-selection.
mutation changeUserName {
changeUser(newName: "Brad") {
name
address {
street
country
}
}
}
If there are two or more root fields in the mutation operation, they
will be executed in sequence, as required by the GraphQL spec.
mutation mutationWithTwoRootFields {
first: changeName(name: "Bradley")
second: changeName(name: "Brad")
}
In this example, the final name of the user will always be “Brad”,
because the fields are always executed in sequence. The second
name change to “Brad” will always be executed last.
Subscription operations
A subscription is a long-lived request that sends updates to the
client when new events happen.
A subscription can only contain exactly one root field, like newEmail.
This is in contrast to query and mutation operations, which can
contain many root fields.
Arguments
Fields can have arguments, which have their type defined in the
schema. Arguments can be either optional or required. As discussed
in the Schema chapter, an argument is required if it is non-null
(indicated with !) and there is no default value (declared with =).
type Pet {
name: String
}
In a query, this is how to request a pet with the string literal “123” as
its id value.
query petSearch {
pet(id: "123") {
name
}
}
Fragments
Fragments allow us to reuse parts of the query. Fragments are a list
of selected fields (including sub-selections) for a certain type.
Fragments have a name and type condition, which must be an
object, interface, or union.
query petOwners {
pets {
owner {
...personDetails
}
previousOwner {
...personDetails
}
}
}
In this example, we use personDetails twice, for both the owner and
previousOwner fields.
Inline fragments
Inline fragments are a selection of fields with a type condition.
Inline fragments are used to query different fields depending on the
type. You can think of them as switch statements, that depend on
the type of the previous field.
interface Pet {
name: String
}
type Dog implements Pet {
name: String
doesBark: Boolean
}
We can write inline fragments to query the doesBark field for Dog
results and doesMeow for Cat results.
query allThePets {
pets {
... on Dog {
doesBark
}
... on Cat {
doesMeow
}
}
}
query invalid {
pets {
doesBark
}
}
type Query {
dinner: [Food]
}
type Pizza {
name: String
toppings: [String]
}
type IceCream {
name: String
flavors: [String]
}
Variables
We can include parameter variables in a GraphQL operation, which
enables clients to reuse operations without needing to dynamically
rebuild them.
An operation can declare variables that serve as input for the entire
operation.
Aliases
By default, a key in the response object will be set as the
corresponding field name in the operation. Aliases enable renaming
of keys in the response.
query searches {
search1: search(filter: "Foo")
search2: search(filter: "Bar")
}
GraphQL document
A GraphQL executable document (often shortened to document) is
a text written in GraphQL query language notation that contains at
least one query, mutation, or subscription operation and an optional
list of fragments.
{
hello
}
mutation changeName {
changeName(name: "Foo")
}
subscription nameChanged {
nameChanged
}
In the first half of this chapter, we will show how to add your data
fetching logic to Spring for GraphQL via controller annotations.
These controller annotations automate much of the work with
DataFetchers, to the point that even the word DataFetcher does not
appear in controller code. In the second half of this chapter, we will
remove the Spring “magic” and take a look under the hood at how
DataFetchers are used by the GraphQL Java engine. By the end of
this chapter, you will have a thorough understanding of
DataFetchers. We will cover more advanced topics in the
DataFetchers in depth chapter and take a deep dive into execution
in the Execution chapter.
Before we go on, there are a few key classes which work closely with
DataFetchers. In GraphQL Java, a GraphQLSchema is both the
structure (or shape) of the API, and all the logic needed to execute
requests against it. Another key GraphQL Java concept is
RuntimeWiring, which contains GraphQLCodeRegistry, a map of schema
fields to DataFetchers. Each DataFetcher needs to be registered in
the GraphQLCodeRegistry inside RuntimeWiring.
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
@Controller
record PetsController( PetService petService) {
@QueryMapping
Pet favoritePet() {
return petService.getFavoritePet();
}
No other changes are required. The keys of the Map match the
schema fields firstName and lastName, so the PropertyDataFetcher will
load the correct values.
DataFetchers and schema mapping
handler methods
A quick note on naming. @SchemaMapping and shortcut annotations
@QueryMapping, @MutationMapping, and @SubscriptionMapping, declare
schema mapping handler methods, which are then registered as
DataFetchers by Spring for GraphQL. There is a one-to-one
relationship between a schema mapping handler method and a
DataFetcher. Therefore, in the remainder of this book, we’ll use the
word DataFetcher for these schema mapping handler methods.
interface Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
Most likely, you will not need to write your own TypeResolvers,
because Spring for GraphQL registers a default
ClassNameTypeResolver which implements the TypeResolver interface.
It tries to match the simple class name of the value to a
GraphQLObjectType. If it cannot find a match, it will continue
searching through super types, including base classes and
interfaces. This default TypeResolver is registered when
graphql.GraphQL is initialized.
For this example schema, to make use of the default type resolver,
create a Pet Java interface, and Dog and Cat classes which implement
Pet.
Then add two owner DataFetchers for Dog and Cat types.
package myservice. service;
@Controller
record PetsController( PetService petService) {
@QueryMapping
Pet favoritePet() {
return petService.getFavoritePet();
}
@SchemaMapping
Person owner( Dog dog) {
return petService.getPerson( dog.ownerId());
}
@SchemaMapping
Person owner( Cat cat) {
return petService.getPerson( cat.ownerId());
}
@Configuration
class Config {
TypeResolver petTypeResolver = ( env) -> {
// Your custom type resolver logic
};
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> {
TypeRuntimeWiring petWiring = newTypeWiring( "Pet")
.typeResolver( petTypeResolver)
.build();
wiringBuilder.type( petWiring);
};
}
With Spring for GraphQL, the arguments are declared with the
@Argument annotation.
@QueryMapping
String search( @Argument String pattern, @Argument int limit) {
// Your search logic here
}
@Controller
class SearchController {
@QueryMapping
String search( @Argument( "pattern") String searchPattern,
@Argument( "limit") int maxElements) {
// Your search logic here
}
input SearchInput {
pattern: String
limit: Int
}
@Controller
class SearchController {
@QueryMapping
String search( @Argument SearchInput input) {
// Your search logic here
}
}
The input object is bound to an instance of SearchInput. This is
easier to work with than the java.util.Map that represents input
objects when using GraphQL Java without Spring for GraphQL.
@Controller
class SearchController {
@QueryMapping
String search( DataFetchingEnvironment env) {
Map< String, Object> input = env.getArgument( "input");
String pattern = ( String) input.get( "pattern");
int limit = ( int) input.get( "limit");
// Your search logic here
}
Argument Description
@Arguments Binding all arguments to a single
object
“Source” Access to the source (parent)
p
Argument instance ofDescription
the field
DataLoader A DataLoader from the
DataLoaderRegistry. See the chapter
about DataLoader
@ContextValue A value from the main
GraphQLContext in
DataFetchingEnvironment.See more
on context in the DataFetchers in
depth chapter
@LocalContextValue A value from the local
GraphQLContext in
DataFetchingEnvironment
type Pet {
"String is a built-in scalar, therefore no declaration is requir
name: String
dateOfBirth: Date
}
To use the Date scalar in the GraphQL Java Extended Scalars library,
add the package.
For Maven:
<dependency >
<groupId >com.graphql-java</groupId >
<artifactId >graphql-java-extended-scalars</artifactId >
<version >19.1</version >
</dependency >
@Configuration
class GraphQlConfig {
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder ->
wiringBuilder.scalar( ExtendedScalars.Date);
}
A DataFetcher loads data for exactly one field. Inside GraphQL Java,
it is represented as a very generic Java interface.
public interface DataFetcher< T> {
T get( DataFetchingEnvironment environment) throws Exception;
}
The interface has only one method get with one argument
DataFetchingEnvironment. The returned result can be anything. This
interface directly reflects a core principle of GraphQL, it is agnostic
about where the data comes from.
Let’s implement DataFetchers for the simple Pet schema in the
earlier Spring for GraphQL example. Note that we are implementing
the initial example, where Pet is an object type and not an interface.
type Query {
favoritePet: Pet
}
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
The Pet record class returned by the PetService is the same as the
class used in the Spring for GraphQL example earlier in this
chapter.
package myservice. service;
As Pet contains only a ownerId and not a full Person object, we need to
load more data. Let’s implement another DataFetcher.
// Lower level layer service does the work of retrieving data
PetService petService = new PetService();
A GraphQLSchema is both the structure of the API, and all the logic
needed to execute requests against it.
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse( s
interface Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
Most likely, you will not need to write nor register any TypeResolvers
and instead make use of Spring for GraphQL’s default
ClassNameTypeResolver. However, we hope this section gives you a
better understanding of how interfaces and unions are resolved in
GraphQL Java.
The best way to get a project up and running with Spring for
GraphQL is via the Spring Initializr tool at https://start.spring.io.
We previously created the application in the “Your first Spring for
GraphQL service” section of the Introduction chapter.
org.springframework.graphql:spring-graphql : Integrates
GraphQL Java with the Spring framework.
org.springframework.boot:spring-boot-starter-graphql : This is
the Spring Boot Starter for GraphQL.
org.springframework.graphql:spring-graphql-test: Testing
support for Spring for GraphQL. We’ll discuss this in the Testing
chapter.
GraphQL Java
GraphQL Java is automatically included with Spring for GraphQL. If
you are using GraphQL Java directly, it can be added to your project
as a dependency via Maven or Gradle.
Every version of GraphQL Java has two parts: the major and bug fix
part. At the time of writing, the latest Spring for GraphQL 1.1.2 uses
GraphQL Java 19.2. GraphQL Java doesn’t use semantic versioning.
or
org.springframework.boot:spring-boot-starter-web
Reading schemas
Spring for GraphQL then scans for schema files in
src/main/resources/graphql/ ending with *.graphqls or *.gqls. After
the schema files are found and successfully loaded, Spring for
GraphQL exposes the GraphQL API at the endpoint /graphql by
default.
Configuration properties
Spring for GraphQL offers configuration properties to adjust the
default behavior without writing any code.
Path: By default, the GraphQL API is exposed via /graphql. This can
be modified by setting spring.graphql.path to another value.
Pet schema
Let’s continue with the GraphQL service we started in the “Your
first Spring for GraphQL service” section of the introduction
chapter, an API for pets. Review the Introduction chapter for
instructions on how to create and download a Spring for GraphQL
service.
type Pet {
name: String
color: String
}
Controllers
Recall from the DataFetchers chapter that DataFetchers load data
for exactly one field. They are the most important concept in
executing a GraphQL request because they represent the logic that
connects your schema and your data.
@Controller
class PetsController {
@QueryMapping
List< Pet> pets() {
return List.of(
new Pet( "Luna", "cappuccino"),
new Pet( "Skipper", "black"));
}
}
The @QueryMapping shortcut annotation registers the pets() method
as a DataFetcher for the query field pets. We did not have to specify
the schema field name, because it was automatically detected from
the method name. For more on @SchemaMapping and shortcut versions
such as @QueryMapping, see the annotated methods section in the
DataFetchers chapter.
@Controller
class PetsController {
WebClient petWebClient;
PetsController( WebClient.Builder builder) {
this.petWebClient = builder.baseUrl( "http://pet-service").bui
}
@QueryMapping
Flux< Pet> pets() {
return petWebClient.get()
.uri( "/pets")
.retrieve()
.bodyToFlux( Pet.class);
}
Source object
A core concept of GraphQL is that you can write flexible queries to
retrieve exactly the information you need.
Let’s expand our Pet schema to model one owner per pet.
type Query {
pets: [Pet]
}
type Pet {
name: String
color: String
owner: Person
}
type Person {
name: String
}
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
@QueryMapping
Flux< Pet> pets() {
return petWebClient.get()
.uri( "/pets")
.retrieve()
.bodyToFlux( Pet.class);
}
// New
@SchemaMapping
Mono< Person> owner( Pet pet) {
return ownerWebClient.get()
.uri( "/owner/{id}", pet.ownerId())
.retrieve()
.bodyToMono( Person.class);
}
The owner DataFetcher returns the owner for exactly one pet. We use
bodyToMono to convert the JSON response to a Java object.
To account for the new Pet schema field owner, we added a new
property ownerId to the Pet class. This ownerId is used to construct
the URL to fetch owner information. Note that the Pet class
contains an ownerId which is not exposed, so a client cannot query it.
GraphQL arguments
Fields can have arguments, which have their type defined in the
schema. Arguments can be either optional or required. As discussed
in the Schema chapter, an argument is required if it is non-null
(indicated with !) and there is no default value (declared with =).
Let’s introduce a new query field pet which takes an argument id.
The argument is of type ID. ID is a built-in scalar type representing a
unique identifier. It is marked as non-nullable by adding !.
type Query {
pets: [Pet]
pet(id: ID!): Pet # New field
}
type Pet {
id: ID! # New field
name: String
color: String
owner: Person
}
type Person {
name: String
}
This is how to query the name of a specific pet with the id argument
“123”.
query myFavoritePet {
pet(id: "123") {
name
}
}
record Pet( String id, String name, String color, String ownerId) {
}
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
@QueryMapping
Flux< Pet> pets() {
return petWebClient.get()
.uri( "/pets")
.retrieve()
.bodyToFlux( Pet.class);
}
@SchemaMapping
Mono< Person> owner( Pet pet) {
return ownerWebClient.get()
.uri( "/owner/{id}", pet.ownerId())
.retrieve()
.bodyToMono( Person.class);
}
// New
@QueryMapping
Mono< Pet> pet( @Argument String id) {
return petWebClient.get()
.uri( "/pets/{id}", id)
.retrieve()
.bodyToMono( Pet.class);
}
type Pet {
id: ID!
name: String
color: String
owner: Person
}
type Person {
name: String
}
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
@QueryMapping
Flux< Pet> pets() {
return petWebClient.get()
.uri( "/pets")
.retrieve()
.bodyToFlux( Pet.class);
}
@SchemaMapping
Mono< Person> owner( Pet pet) {
return ownerWebClient.get()
@QueryMapping
Mono< Pet> pet( @Argument String id) {
return petWebClient.get()
.uri( "/pets/{id}", id)
.retrieve()
.bodyToMono( Pet.class);
}
// New
@QueryMapping
Flux< Pet> petSearch( @Argument PetSearchInput input) {
// perform the search
}
}
Mutations
As we saw in the Query Language chapter, data is changed in
GraphQL with mutation operations. Let’s add a mutation to change
a pet’s name.
type Query {
pets: [Pet]
pet(id: ID!): Pet
petSearch(input : PetSearchInput!): [Pet]
}
# New type
type ChangePetNamePayload {
pet: Pet
}
input PetSearchInput {
namePattern: String
ownerPattern: String
}
type Pet {
id: ID!
name: String
color: String
owner: Person
}
type Person {
name: String
}
The return type for the mutation field ends with Payload to follow a
quasi-standard naming convention for mutation response types.
This is a mutation request to change the name of the pet with the id
“123” to “Mixie”.
mutation changeName {
changePetName(id: "123", newName: "Mixie") {
pet {
name
}
}
}
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
@QueryMapping
Flux< Pet> pets() {
return petWebClient.get()
.uri( "/pets")
.retrieve()
.bodyToFlux( Pet.class);
}
@SchemaMapping
Mono< Person> owner( Pet pet) {
return ownerWebClient.get()
.uri( "/owner/{id}", pet.ownerId())
.retrieve()
.bodyToMono( Person.class);
}
@QueryMapping
Mono< Pet> pet( @Argument String id) {
return petWebClient.get()
.uri( "/pets/{id}", id)
.retrieve()
.bodyToMono( Pet.class);
}
@QueryMapping
Flux< Pet> petSearch( @Argument PetSearchInput input) {
// perform the search
}
// New
@MutationMapping
Mono< ChangePetNamePayload> changePetName(
@Argument String id,
@Argument String newName
) {
Map< String, String> changeNameBody = Map.of(
"name", newName
);
return petWebClient.put()
.uri( "/pets/{id}", id)
.contentType( MediaType.APPLICATION_JSON)
.body( BodyInserters.fromValue( changeNameBody))
.retrieve()
.bodyToMono( ChangePetNamePayload.class);
}
# New
type Human {
name: String
}
# New
union Creature = Dog | Cat | Human
Note that the GraphQL spec requires that unions only contain
object types. We must specify the object types Dog and Cat, we
cannot specify the interface Pet.
Let’s mirror these changes in our Java model. Let’s represent Pet as
an interface.
package myservice. service;
interface Pet {
String id();
String name();
String color();
}
And create two new classes, Dog and Cat, which implement the Pet
interface.
package myservice. service;
@Controller
class PetsController {
@QueryMapping
Flux< Object> creatures() {
// Add your fetching logic
// In-memory example
return Flux.just(
new Dog( "Dog01", "Spot", "Yellow", true),
new Cat( "Cat01", "Chicken", "Orange", true),
new Human( "Donna"));
Try out the query below with the GraphiQL interactive playground
at http://localhost:8080/graphiql. To enable GraphiQL, please add
the following to your application.properties file.
spring.graphql.graphiql.enabled=true
query allTheThings {
creatures {
...on Dog {
name
barks
}
... on Cat {
name
meows
}
... on Human {
name
}
}
}
Note how there are multiple responses for the single subscription
request. This differs from queries and mutations, where one request
corresponds to exactly one response.
Getting started
Let’s implement subscriptions with Spring for GraphQL.
Start with a new Spring for GraphQL project with Spring Initializr at
https://start.spring.io/, as we did in the “Your first Spring for
GraphQL service” section of the Introduction chapter. Choose
Spring for GraphQL as a dependency, and choose either Spring Web
or Spring Reactive Web as a dependency. In the following examples
we will use Spring Reactive Web, which includes Spring WebFlux. If
you prefer to use subscriptions with WebMVC instead of WebFlux,
add org.springframework.boot:spring-boot-starter-websocket as a
dependency, and adjust the examples by removing Mono and Flux.
type Subscription {
hello: String
}
@Controller
class HelloController {
@SubscriptionMapping
Flux< String> hello() {
Flux< Integer> interval = Flux.fromIterable( List.of( 0, 1, 2))
.delayElements( Duration.ofSeconds( 1));
return interval.map( integer -> "Hello " + integer);
}
}
Let’s test our subscription. Start the service and open the GraphiQL
playground at http://localhost:8080/graphiql.
Then we will see the response changing every second, from “Hello
0” to “Hello 1” to “Hello 2”, as shown in the screenshot “Hello 2
answer”.
Hello 2 answer
Execution
Similarly to queries and mutations, we implement subscriptions as
DataFetchers. In order to deliver a stream of responses, GraphQL
Java requires that it return a org.reactivestreams.Publisher instance.
The Reactive Streams initiative defines this interface to provide a
standard for asynchronous stream processing.
When using GraphQL Java with Spring, we use Flux from Project
Reactor, which implements Publisher. We used Flux in the example
earlier in this chapter.
We send the result back to the client. When the Publisher emits a
new event, the execution starts again, and we send a new result to
the client.
Once the Publisher signals that it has finished, the whole request
finishes.
It’s important to note that the data emitted by the Publisher is not
the actual data sent to the client, but only used as input for the sub-
selection, which follows the same GraphQL execution rules as
queries and mutations.
Protocol
Subscriptions require a way for the server to inform the client about
new events. The protocol that comes closest to a standard for
subscriptions is a WebSocket-based protocol: graphql-ws. Spring for
GraphQL supports this protocol out of the box.
spring.graphql.websocket.path=/graphql-subscriptions
Client support
We can use the graphql-ws protocol with a variety of different
clients. However, as some clients might not support graphql-ws by
default, additional setup might be required. The graphql-ws GitHub
repo contains a list of recipes for different clients.
In this chapter we discussed GraphQL subscriptions. Later in the
Testing chapter, we’ll discuss how to test subscriptions.
Request and response
In this chapter, we will take a closer look at requests and responses
for GraphQL, including the HTTP protocol.
Request
The most important elements of a GraphQL request are the query,
operation name, and variables. Every GraphQL request over HTTP
is a POST encoded as application/json, with the body being a JSON
object:
{
"query": "<document>",
"variables": {
<variables>
},
"operationName": "<operationName>"
}
The next key “variables” is a JSON map with all the variables for the
operation. This key is optional, as operation variables are optional.
Response
Over HTTP, the response to a GraphQL request is a JSON object:
{
"data": <data>,
"errors": <list of GraphQL errors>,
"extensions": <map of extensions>
}
If the data key is not present, the errors key must be present to
explain why no data was returned. If the data key is present, the
errors key can be present too, in the case where partial results are
returned. Note that null is a valid value for data.
The extensions key is optional. The value is a map and there are no
restrictions on its contents.
Under the hood in GraphQL Java, the response is represented as
graphql.ExecutionResult, which mirrors the JSON response.
Why would we return a 200 OK code even when there are errors in
the response? The reason for this model is to enable more flexible
requests and partial responses compared to a REST API. For
example, a partial response is still valuable, so it is returned with a
200 OK status code, and errors in the response to explain why part of
the data could not be retrieved.
The challenge with this model is that analyzing the response now
requires two steps:
1. Check the HTTP status code
2. If the status code is 200 OK, check for any GraphQL errors in the
response
HTTP headers
We strongly recommend that information needed to understand the
request should be part of the GraphQL operation, and not passed in
via request headers.
This general rule comes from the intention to express the API
completely in the GraphQL schema. Let’s demonstrate this via a
counterexample, where an HTTP header “Customer-Id” is sent
alongside a query for the field ordersByCustomerId. In the schema, the
field is defined as:
type Query {
ordersByCustomerId: [Order]
}
Imagine you are reading the schema for the first time, wanting to
understand the API. There is no information to indicate that a
“Customer-Id” header is essential for the ordersByCustomerId field.
The schema becomes an incomplete description of the API.
Intercepting requests
Spring for GraphQL provides WebGraphQlInterceptor to intercept
requests.
@Component
class MyInterceptor implements WebGraphQlInterceptor {
@Override
public Mono< WebGraphQlResponse> intercept(
WebGraphQlRequest request, Chain chain
) {
return chain.next( request)
.map( response -> {
response.getResponseHeaders().add( "special-header", "true
return response;
});
}
}
@Component
class BetaFeaturesInterceptor implements WebGraphQlInterceptor {
@Override
public Mono< WebGraphQlResponse> intercept(
WebGraphQlRequest request, Chain chain
) {
boolean betaFeatures = request
.getHeaders()
.containsKey( "beta-features");
@Component
class ChangeResponse implements WebGraphQlInterceptor {
@Override
public Mono< WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain
) {
return chain.next( request)
.map( response -> {
// response is a WebGraphQLResponse containing
// the ExecutionResult
}
In this chapter we discussed requests and responses for GraphQL in
greater detail, and demonstrated how to access these objects in
Spring for GraphQL.
There are broadly two kinds of GraphQL errors: request errors and
field errors. We’ll walk through how GraphQL errors are presented
with examples.
Request errors
A request error is raised during a request. The GraphQL response
will contain an errors key, but no data key. For example, a request
error will be raised if a request contains a GraphQL syntax error,
such as a missing closing curly brace }.
Request errors are raised before execution begins. In other words,
request errors are raised before any DataFetchers are invoked. A
request error is usually the fault of the requesting client.
The GraphQL spec also allows for an optional key extensions, which
is a map of additional data. There are no restrictions on the contents
of this map. It’s useful for error logging to categorise errors, so
GraphQL Java provides a number of common error classifications.
On top of this, Spring for GraphQL adds a few extra error
classifications. You can also create custom error classifications.
We’ll explain classifications in more detail later in this chapter. In
this example, the InvalidSyntax classification was added by GraphQL
Java.
Note how there was no data key in the GraphQL response, because
no DataFetchers were invoked. Execution was terminated when the
syntax error was detected.
{
"errors": [
{
"message": "Validation error (FieldUndefined@[doesNotExist]) :
Field 'doesNotExist' in type 'Query' is undefined",
"locations": [
{
"line": 2,
"column": 5
}
],
],
"extensions": {
"classification": "ValidationError"
}
}
]
}
The message communicates that the field doesNotExist does not exist
in the Query type. As this error can be linked to a location in the
GraphQL document, it is provided.
Note that there was no data key in the GraphQL response, because
no DataFetchers were invoked. Execution was terminated when the
validation error was detected.
Field errors
Field errors are raised during the execution of a field, resulting in a
partial response. In other words, an error raised during the
execution of a DataFetcher.
type Pet {
id: ID
name: String
friends: [Pet]
}
We make a request with this query:
query whoIsAGoodPup {
favoritePet {
name
friends {
name
}
}
}
@Controller
class PetsController {
@QueryMapping
Pet favoritePet() {
// Logic to return the user's favorite pet.
// Logic mocked with Luna the Dog.
return Pet.pets.get( 0);
}
@SchemaMapping
List< Pet> friends( Pet pet) {
throw new RuntimeException( "Something went wrong!");
}
}
Our example demonstrates that field errors don’t cause the whole
request to fail, meaning a GraphQL result can contain “partial
results”, where part of the response contains data, while other parts
are null. We were able to load Luna’s name, but none of her friends.
Because we were unable to load friends, the “friends” key has the
value null.
Partial results have consequences for the client. Clients must always
inspect the “errors” of the response in order to determine whether
an error occurred or not. Note that you cannot rely on a null value
to indicate a GraphQL error was raised, instead the errors key of the
response must always be inspected. A DataFetcher can return both
data and errors for a given field.
We have one error with a “message” key, representing the exception
thrown inside the friends DataFetcher. The “locations” key
references the position of friends in the query and “path” of the field
that caused the error.
The GraphQL spec defines a few rules for when data and errors are
present in the response.
The errors entry will be present if there are errors raised during
the request. If there are no errors raised during the request,
then the errors entry must not be present. If the errors entry is
present, it is a non-empty list.
If the data entry of the response is not present, the errors entry
must be present. For example, a request error will have no data
entry in the response, so the errors entry must be present.
If the data entry of the response is present (including the value
null), the errors entry must be present if and only if one or more
field errors were raised during execution.
String getMessage();
List< SourceLocation> getLocations();
List< Object> getPath();
Map< String, Object> getExtensions();
ErrorClassification getErrorType();
Error classifications
Classifying errors is useful for logging and monitoring. GraphQL
Java enables error classifications to be added to responses. Note
that although classifying errors is not required by the GraphQL
spec, we have found it invaluable for categorizing errors in metrics.
Classification Description
InvalidSyntax Request error due to invalid
GraphQL syntax
ValidationError Request error due to invalid
request
OperationNotSupported Request error if request attempts
to perform an operation not
defined in the schema
DataFetchingException Field error raised during data
fetching
NullValueInNonNullableField Field error when a field defined
as non-null in the schema
returns a null value
BAD_REQUEST
UNAUTHORIZED
FORBIDDEN
NOT_FOUND
INTERNAL_ERROR
i h l h
import graphql. GraphQLError;
import graphql. GraphqlErrorBuilder;
import graphql. schema. DataFetchingEnvironment;
import org. springframework. graphql. execution
.DataFetcherExceptionResolverAdapter;
import org. springframework. graphql. execution. ErrorType;
import org. springframework. stereotype. Component;
@Component
class CustomErrorMessageExceptionResolver
extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError( Throwable ex,
DataFetchingEnvironment env) {
return GraphqlErrorBuilder.newError( env)
.errorType( ErrorType.INTERNAL_ERROR) // Error classificat
.message( "My custom message") // Overrides the message
.build();
}
}
If you are using GraphQL Java without Spring for GraphQL, note
that the handler in GraphQL Java is different. GraphQL Java uses
the SimpleDataFetcherExceptionHandler implementation. This handler
creates a ExceptionWhileDataFetching error with the classification
ErrorType.DataFetchingException.
For example, we have a list of some pets, but not all of it was
available during execution. Let’s take a look at a simple Pet schema.
type Query {
myPets: [Pet]
}
type Pet {
id: ID
name: String
}
package myservice. service;
@Controller
class PetsController {
@QueryMapping
DataFetcherResult< List< Pet>> myPets(
DataFetchingEnvironment env) {
// Your partial list of data here
// In-memory Pet example
List< Pet> result = List.of( Pet.pets.get( 1));
For example, just because the current database backing the API
doesn’t allow the User.name field to be null doesn’t automatically
mean it should also be non-nullable in the schema. The design
needs to be justified independent of the current implementation.
type User {
id: ID!
name: String
}
type User {
id: ID!
name: String
address: Address
}
type Address {
street: String
city: String
country: String
}
This schema change makes our API is richer, clients can choose
whether to use the new functionality by including an address in
their user query selection fields or not.
type User {
id: ID!
name: UserName
}
type UserName {
legalName: String
preferredName: String
}
However, this schema change breaks all existing clients, who are
using name, such as this query.
query broken {
user(id: "123") {
name
}
}
The query would suddenly become invalid and always result in an
error because name is no longer a User string field, and now it’s a
UserName object that needs a sub-selection.
First, we add a new field for userName, while leaving the existing
User field there for now.
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String
userName: UserName
}
type UserName {
legalName: String
preferredName: String
}
type UserName {
legalName: String
preferredName: String
}
type User {
id: ID!
userName: UserName
}
type UserName {
legalName: String
preferredName: String
}
Connected
A GraphQL API should resemble a connected or graph-like structure
for maximum client flexibility. The client should be able to
“traverse” from one piece of data to another related one, in a single
request.
type Issue {
description: String
ownerId: ID
}
type User {
id: ID
name: String
}
This schema requires two queries to retrieve the owner’s name for
an issue.
# First
query myIssue {
issue {
ownerId
}
}
# returns "123"
# Second
query myUser {
userById(id: "123") {
name
}
}
type Issue {
description: String
owner: User
}
type User {
id: ID
name: String
}
Now the client can directly query the full User object for the issue in
one query.
query connected {
issue {
owner {
name
}
}
}
For example, you might consider reusing input objects like this
search filter.
type Query {
searchPets(filter: SearchFilter): [Pet]
searchHumans(filter: SearchFilter): [Human]
}
input SearchFilter {
name: String
ageMin: Int
ageMax: Int
}
Reusing the input object is not a good idea because it couples the
two fields unnecessarily together. What happens if we would like to
add a breed field for pets? Now we have either a filter for humans
that includes a breed, or we need to deprecate fields and introduce
new ones.
The same principle is true for output types. This example can seem
tempting especially for mutations.
type Mutation {
deleteUser(input : DeleteUserInput!): ChangeUserPayload
updateUser(input : UpdateUserInput!): ChangeUserPayload
}
type ChangeUserPayload {
user: User
}
This example has the same problem as the reused input objects.
Once we want to change the return type for just one mutation, we
have a problem.
The other trap we might fall into is trying to combine multiple use
cases into one field. Fields are cheap, like any other element. Our
service doesn’t get slower, or have any other direct negative effects
with a larger amount of fields. We should make single-purpose
fields explicit with specific naming.
vs
type Query {
petById(id: ID!): Pet
petByName(name: String!): Pet
}
type A {
b: B
}
type B {
c: C!
}
type C {
d: String!
}
If the error propagates all the way up, we set everything to null and
we even lose the result of other root fields. Let’s walk through a
more realistic example.
type Pet {
name: String
}
type Human {
name: String
}
This looks innocent, but if we query pet and human at the same time,
query petAndHuman {
pet {
name
}
human {
name
}
}
and if pet field fails to load, but human field load succeeds, we still
end up with no data.
{
"data": null
}
Sometimes there are other fields that we should also make non-
nullable. A User could have a primary email to login in, but it is
reasonable to assume that this is such an important field that we
don’t want to serve any data if we can’t load the primary email.
type User {
id: ID!
primaryEmail: String! # also non-null
name: UserName
address: Address
}
type Order {
id: ID!
customer: Customer!
# And more order fields here
}
type Customer {
id: ID!
}
Perhaps this schema looks fine because we store all the current
orders in one database and if we can load an order, then we also
load the customer at the same time. But now imagine that we
change our architecture in the future and decide to introduce an
order service and a separate customer service. Suddenly we have a
situation where we could load the order, but not the customer,
resulting in the whole Order being null when the error propagates
up.
The root field itself is nullable, but the elements inside the list are
not nullable. Even if we take current or future implementations into
consideration where we could load some orders, but not all, we
mostly don’t want to burden the client with special error handling.
Both arguments are nullable and the field itself is too generic. It is
better to change fields that must be present to be non-nullable.
As with fields, the elements inside a list are often an excellent good
candidate for making non-nullable.
A simple list such as pets: [Pet] can quickly become too large for
clients to handle. A simple list restricts clients to only two options:
requesting all the data, or none at all. If requested, the list will be
returned in its entirety, regardless of the size.
Schema
This is an example Pet schema implementing the Relay connections
specification. We’ll go into further details of the specification after
walking through example queries.
type Query {
pets(first: Int, after: String, last: Int, before: String):
PetConnection
}
type PetConnection {
edges: [PetEdge]
pageInfo: PageInfo!
}
type PetEdge {
cursor: String!
node: Pet!
}
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type Pet {
name: String
# Your additional Pet fields here
}
Let’s start at the top of the query. The first argument limits the
result to a maximum of 2 elements. We don’t supply any cursor
argument, because we don’t yet have a cursor.
In the next layer of the query are our connection fields, edges and
pageInfo.
Connection
A connection represents a page of data (edges), with additional
metadata to enable further page requests (pageInfo).
As we saw in the previous query examples, first and last are of type
Int because they represent how many objects we want to request.
after and before are of type String because they are cursors that
identify a position within a list of elements.
type Query {
pets(
first: Int,
after: String,
last: Int,
before: String,
namePattern: String
): PetConnection
}
The <Entity>Connection type must have at least the two fields edges
and pageInfo.
type PetConnection {
edges: [PetEdge]
pageInfo: PageInfo!
}
Edges
The edges of a connection represent a page of data. An edge is a
wrapper object that contains a data element (e.g. a Pet) and
metadata. It is named in the format <Entity>Edge. It must have at
least two fields: the cursor for the current element and the actual
element named node. Optionally, additional fields can be added.
type PetEdge {
cursor: String!
node: Pet!
}
PageInfo
This allows us to directly query pet data with nodes rather than via
edges. We retrieve the startCursor and endCursor for the page, rather
than the cursor for every pet as we did in the initial pagination
response example.
query shortcut {
pets(first: 2) {
nodes {
name
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
Expected errors
In the GraphQL errors chapter, we discussed errors appearing in the
GraphQL response, typically arising from unexpected issues such as
a database not being reachable or bugs, but they are not well suited
for expected errors. Expected errors are situations that the client
wants to handle specifically. In this section we’ll demonstrate best
practice for managing expected errors.
For example, an error for invalid payment details could look like
this.
{
"data": {
"makePayment": null
},
"errors": [{
"message": "Payment failed",
"extensions": {
"classification": "PAYMENT_ERROR",
"details": "Invalid credit card"
}
}]
}
A client now has to parse the “message” and potentially also look at
the “extensions”, which are untyped and can contain any data.
Example:
type Mutation {
makePayment(input : MakePaymentInput!): MakePaymentPayload
}
type MakePaymentPayload {
payment: Payment
error: MakePaymentError
}
enum MakePaymentError {
CC_INVALID,
PAYMENT_SYSTEM_UNAVAILABLE
}
This brings the payment errors into the response type, which
appears in the data section of the GraphQL response. Note how
these errors are no longer in the errors section of the GraphQL
response.
{
"data": {
"makePayment": {
"payment": null,
"error": "CC_INVALID"
}
}
}
type Pet {
# Your Pet fields here
}
type PetLookupError {
# Your PetLookupError fields here
}
We can then use inline fragments to handle the result and error
cases.
query myPet {
pet(id: "123") {
... on Pet {
# Your Pet fields here
}
... on PetLookupError {
# Your PetLookupError fields here
}
}
}
Mutation format
The GraphQL community mostly uses a specific format for
mutation, which comes originally from Relay.
Global context
In GraphQL Java, GraphQLContext is a mutable map containing
arbitrary data, which is made available to every DataFetcher. It
provides a “global context” per execution. In Spring for GraphQL,
the global GraphQLContext can be accessed by adding it as a method
parameter to a schema mapping handler or batch mapping method.
In pure GraphQL Java, it can be accessed via
ExecutionInput.getGraphQLContext(). For example, let’s say we want to
make a “userId” accessible to every DataFetcher. In Spring for
GraphQL, we can access GraphQLContext via a schema mapping or
batch mapping method parameter and add a userId:
@SchemaMapping
MyType myField( GraphQLContext context) {
context.put( "userId", 123);
// Your logic here
}
@Component
class UserIdInterceptor implements WebGraphQlInterceptor {
@Override
public Mono< WebGraphQlResponse> intercept(
WebGraphQlRequest request, Chain chain
) {
request.configureExecutionInput(( executionInput, builder) ->
executionInput
.getGraphQLContext()
.put( "userId", "123");
return executionInput;
});
Local context
It’s also possible to set local context which only provides data to
child DataFetchers, rather than changing global context.
For example, we have the following schema for customers and their
orders.
type Query {
order: Order
customerById(id: ID!): Customer
}
type Order {
id: ID
customer: Customer
}
type Customer {
id: ID
contact: Person
}
type Person {
name: String
}
or
query orderDetails {
order {
customer {
contact {
name
}
}
}
}
Imagine that our persistence layer stores the full customer next to
the order. That means, when we load an order, we have already
loaded the full customer including their contact information.
However, in our persistence layer, a customer loaded directly does
not include the corresponding contact information.
For queries including the order field, we can avoid a second fetch for
customer contact information by setting the local context when
loading the order and make use of it in the customer contact
DataFetcher.
@Controller
record OrderController( OrderService orderService,
PersonService personService) {
@QueryMapping
DataFetcherResult< Order> order() {
Order order = orderService.getOrder();
Person personForContact = order.getPersonForContact();
// Local instance of GraphQLContext
GraphQLContext localContext = GraphQLContext.newContext()
.put( "personForContact", personForContact)
.build();
// Return data and a new local context
return DataFetcherResult.< Order>newResult()
.data( order)
.localContext( localContext)
.build();
}
@SchemaMapping
Customer customer( Order order) {
return orderService.getCustomer( order);
}
@SchemaMapping
Person contact( Customer customer, DataFetchingEnvironment env) {
GraphQLContext localContext = env.getLocalContext();
if ( localContext != null
&& localContext.get( "personForContact") instanceof Person) {
return localContext.get( "personForContact");
}
return personService.getPerson( customer.contactId());
}
@QueryMapping
Customer customerById( @Argument String id) {
return orderService.getCustomerById( id);
}
If you are certain that the local context will always contain a
particular key, you can pass in a parameter to the schema mapping
method, annotated with @LocalContextValue. In this example, we
could not use this annotation, as a runtime exception would be
raised whenever personForContact is not set.
Then the DataFetcher for Customer.contact can make use of the pre-
loaded Person in local context, if it is available. If the Person is not
available, a request to the Person service will be made.
// DataFetcher for Customer.contact
PersonService personService;
DataFetcher< Person> contactDf = ( env) -> {
// If we already loaded the person earlier
if ( env.getLocalContext() instanceof Person) {
return env.getLocalContext();
}
Customer customer = env.getSource();
return personService.getPerson( customer.getContactId());
};
We will discuss these three patterns and when to use them. As the
next few examples demonstrate DataFetcher patterns, we will show
snippets rather than a full Spring for GraphQL controller.
Non-reactive DataFetcher
In a reactive service, this is still a valid option if the work is only fast
computation work, meaning no I/O is involved. The exact definition
of “fast” is domain-specific, but as a rough guide, “fast” would be
work that takes less than one millisecond to complete.
Although wrapping an I/O call does not make the whole service
completely reactive, it may still be worth doing as it doesn’t block
GraphQL Java itself and allows for parallel fetching of fields.
Reactive I/O
ReactiveClient client;
DataFetcher< CompletableFuture< Something> df = ( env) -> {
return client.call();
};
Reactive or not?
Whether to use reactive patterns is a general question, which is not
specific to GraphQL. Here are some high-level considerations to
keep in mind.
Spring for GraphQL supports the Reactor types Mono and Flux as
return values, which enables us to write reactive DataFetchers. If
the words Mono and Flux are new to you, please see the Reactor
documentation.
We recommend using the Reactor types Mono and Flux rather than
Java’s CompletableFuture with Spring for GraphQL to make use of
Reactor context. However, it is still possible to return
CompletableFuture values.
To use Spring WebFlux, include org.springframework.boot:spring-
boot-starter-webflux as a dependency. We previously walked
through how to use Spring WebFlux in the Building a GraphQL
service chapter.
or
@Override
public Mono< WebGraphQlResponse> intercept( WebGraphQlRequest requ
Chain chain) {
return chain.next( request)
.contextWrite( Context.of( "loggingPrefix", "123"));
}
Built-in directives
The GraphQL spec defines four built-in directives, which must be
supported by all GraphQL implementations. Built-in directives can
be used without being declared. Later in this chapter, we’ll see how
to declare and implement our own directives.
# same as:
query myPets2 {
pets {
name
age @skip(if: true)
}
}
# same as:
query myPets3 {
pets {
name
age @include(if: false)
}
}
To be more useful, @skip and @include should be combined with
variables rather than hard coded booleans. For example, we could
include an experimental field based on a variable value:
query myQuery($someTest: Boolean!) {
experimentalField @include(if: $someTest)
}
@deprecated
@deprecated is a schema directive that can be used to mark fields,
enum values, input fields, and arguments as deprecated in the
schema. It provides a structured way to document deprecations. By
default, the introspection API filters out deprecated schema
elements.
enum Format {
LEGACY @deprecated(reason: "Legacy format")
NEW
}
@specifiedBy
@specifiedBy allows us to provide a scalar specification URL to
describe the behavior of custom scalar types.
With the GraphQL Scalars project, you can create your own custom
scalars specifications and host them on the GraphQL Foundation’s
scalars.graphql.org domain, like the linked URL in the previous
example. You can also read and link to other contributed
specifications. See the GraphQL Scalars project for more
information.
All custom schema and operation directives don’t have any effect
until we implement new custom behavior. The @important directive
won’t have any effect until we implement new logic, which we’ll
cover later in this chapter. This differs from the built-in directives,
which all have a well-defined effect.
type Query {
pet: Pet
}
type Pet {
name: String
lastTimeOutside: String
}
query someQuery(
$var: String @foo # Variable definition
) @foo # Query
{
field @foo # Field
... on Query @foo { # Inline fragment
field
}
...someFragment @foo # Fragment spread
}
Repeatable directives
We can define schema and operation directives as repeatable,
enabling it to be used multiple times in the same location. If
repeatable is not included in the directive definition, the directive
will be non-repeatable by default.
type Query {
# Multiple owners per field possible
hello: String @owner(name: "Brian") @owner(name: "Josh")
}
@Controller
class GreetingController {
@QueryMapping
String hello( DataFetchingEnvironment env) {
GraphQLFieldDefinition fieldDefinition = env.getFieldDefinitio
GraphQLAppliedDirective important
= fieldDefinition.getAppliedDirective( "important");
if ( important != null) {
if ( important != null) {
return handleImportantFieldsDifferently( env);
}
return "Hello";
}
}
For Maven:
<dependency >
<groupId >com.graphql-java</groupId >
<artifactId >graphql-java-extended-validation</artifactId >
<version >19.1</version >
</dependency >
= objectType.getAppliedDirective( "owner");
if ( directive != null) {
String owner = directive.getArgument( "name").getValue();
ownerToTypes.putIfAbsent( owner, new ArrayList<>());
ownerToTypes.get( owner).add( objectType);
}
return TraversalControl.CONTINUE;
}
}, schema);
This @owner example was more like a script rather than core
functionality in a Spring for GraphQL service. However, if you want
to traverse a schema in Spring for GraphQL, you can register
graphql.schema.GraphQLTypeVisitor via the GraphQlSource.builder with
builder.schemaResources(..).typeVisitors(..).
@Override
public TraversalControl visitGraphQLFieldDefinition(
GraphQLFieldDefinition fieldDefinition,
TraverserContext< GraphQLSchemaElement> context
) {
GraphQLSchemaElement parentNode = context.getParentNode();
if (!( parentNode instanceof GraphQLObjectType)) {
return TraversalControl.CONTINUE;
}
GraphQLObjectType objectType = ( GraphQLObjectType) parentNode;
GraphQLAppliedDirective directive = objectType
.getAppliedDirective( "suffix");
if ( directive != null) {
String suffix = directive.getArgument( "name").getValue();
GraphQLFieldDefinition newFieldDefinition
= fieldDefinition.transform( builder
-> builder.name( fieldDefinition.getName() + suffix));
return changeNode( context, newFieldDefinition);
}
return TraversalControl.CONTINUE;
}
});
We visit every field definition and try to get the object containing
the field via context.getParentNode(). Then we get the
GraphQLAppliedDirective for the suffix. We use this to create a
GraphQLFieldDefinition with the changed name. The last thing to do
is to call changeNode (from GraphQLTypeVisitor) which actually
changes the field.
To use this same schema transformation example in Spring for
GraphQL, register a graphql.schema.GraphQLTypeVisitor via the
GraphQlSource.Builder with
builder.schemaResources(..).typeVisitorsToTransformSchema(..).
For example, a client specifies that hello cache entries must not be
older than 500 ms, otherwise we re-fetch these entries.
query caching {
hello @cache(maxAge: 500)
}
In GraphQL Java, operation directive definitions are represented as
GraphQLDirectives. Operation directive usages are represented as
QueryAppliedDirectives. Note that the word “query” here is
misleading, as it actually refers to a directive that applies to any of
the three GraphQL operations: queries, mutations, or subscriptions.
Operation directives are still commonly referred to as “query”
directives, hence the class name.
@Controller
class GreetingController {
@QueryMapping
String hello( DataFetchingEnvironment env) {
QueryDirectives queryDirectives = env.getQueryDirectives();
List< QueryAppliedDirective> cacheDirectives = queryDirectives
.getImmediateAppliedDirective( "cache");
// We get a List, because we could have
// repeatable directives
if ( cacheDirectives.size() > 0) {
QueryAppliedDirective cache = cacheDirectives.get( 0);
QueryAppliedDirectiveArgument maxAgeArgument
= cache.getArgument( "maxAge");
int maxAge = maxAgeArgument.getValue();
@Configuration
class GraphQlConfig {
@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return ( builder) ->
builder.configureGraphQl( graphQlBuilder ->
// Here we can use `GraphQL.Builder`
// For example, executionIdProvider
graphQlBuilder
.executionIdProvider(new MyExecutionIdProvid
}
}
If you are using GraphQL Java without Spring for GraphQL, this is
how to manually initialize the graphql.GraphQL object.
String sdl = "type Query { foo: String }"; // Your schema here
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse( sdl);
Recall from the request and response chapter that Spring for
GraphQL automatically handles the HTTP protocol. A GraphQL
request is an HTTP POST encoded as application/json.
If you are using GraphQL Java without Spring for GraphQL, this is
how to manually create an ExecutionInput and execute the request.
GraphQL graphQL = GraphQL.newGraphQL( schema).build();
Execution steps
In GraphQL Java, execution includes the following steps:
These are the steps between the GraphQL Java engine receiving a
ExecutionInput request and returning a ExecutionResult instance with
data and/or errors.
The first step is parsing the “query” (document) value from the
ExecutionInput and validating it. If the document contains invalid
syntax, the parsing fails immediately. Otherwise, if the document is
syntactically valid, it then is validated against the schema.
Coercing variables
See the query language chapter for an overview of GraphQL
variables and see how variables are sent in an HTTP request in the
request and response chapter.
This query has one variable $name with the type String. If the request
now contains the following variables, variable coercing would fail
since we expect a single String for name, not a list of Strings.
{
"name": ["Luna", "Skipper"]
}
Fetching data
The last step is the core of execution: GraphQL Java fetching the
data needed to fulfill the request.
Let’s look more closely at an example schema and query, which will
help us understand the overall execution algorithm.
type Query {
dogs: [Dog]
}
type Dog {
name: String
owner: Person
friends: [Dog]
details: DogDetails
}
type Person {
firstName: String
lastName: String
}
type DogDetails {
barking: Boolean
shedding: Boolean
}
query myDogs {
dogs {
name
owner {
firstName
lastName
}
friends {
name
}
details {
barking
shedding
}
}
}
Tree of Fields
Execution Order
Reactive concurrency-agnostic
GraphQL Java is reactive concurrency-agnostic. This means
GraphQL Java doesn’t prescribe a specific number of threads nor
when they are used during execution. This is achieved by leveraging
java.util.concurrent.CompletableFuture. Every DataFetcher can
return a CompletableFuture, or if not, GraphQL Java wraps the
returned value into a CompletableFuture.
Here are a few DataFetcher examples to make this clear. Note that it
is equivalent in Spring for GraphQL to implement these
DataFetchers via controller methods annotated with @SchemaMapping.
// Intensive compute work in the current thread
DataFetcher< String> df = ( env) -> {
String str = intensiveComputeString();
return str;
};
Completing a field
After a DataFetcher returns a value for a field, GraphQL Java needs
to process it. This phase is called “completing a field”.
For scalars and enums, the value is “coerced”. Coercing has two
different purposes: first is making sure the value is valid, the second
one is converting the value to an internal Java representation. Every
GraphQLScalarType references a graphql.schema.Coercing instance. For
enums, the GraphQLEnumType.serialize method is called.
TypeResolver
If the type of the field is an interface or union, GraphQL Java needs
to determine the actual object type of the value via a TypeResolver.
See the DataFetchers chapter for an introduction to TypeResolvers
and how to use them in Spring for GraphQL and GraphQL Java.
This section focuses on the execution of TypeResolvers.
interface Pet {
name: String
}
Here the sub-selection for pet is { ...on Dog { barks } ...on Cat {
meows } }. If the returned value from the DataFetcher is a Dog, we
need to fetch the field barks; if it is a Cat, we need to fetch meows.
Then the DataFetcher for all the fields in the sub-selection are
called, as explained in the previous sections. This is a recursive step,
which then again leads to the completion of each of the fields.
Query vs mutation
Queries and mutations are executed in an almost identical way. The
only difference is that the spec requires serial execution for multiple
mutations in one operation.
For example:
mutation modifyUsers {
deleteUser( ... ) { ... }
addOrder( ... ) { ... }
changeUser( ... ) { ... }
}
vs
query getUsersAndOrders {
searchUsers( ... ) { ... }
userById( ... ) { ... }
allOrders { ... }
}
@Configuration
class MyGraphQLConfiguration {
@Bean
MaxQueryDepthInstrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation( 15);
}
}
@Component
class LogTimeInstrumentation extends SimpleInstrumentation {
@Override
public InstrumentationContext< ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
return new InstrumentationContext<>() {
AtomicLong timeStart = new AtomicLong();
@Override
@Override
public void onDispatched(
CompletableFuture< ExecutionResult> result) {
timeStart.set( System.currentTimeMillis());
}
@Override
public void onCompleted( ExecutionResult result, Throwable t)
System.out.println( "execution time: "
+ ( System.currentTimeMillis() - timeStart.get()));
}
};
}
}
At the time of writing, the latest Spring for GraphQL 1.1 uses
GraphQL Java 19.x. In Spring for GraphQL 1.2, GraphQL Java 20.x
will be used, which adds the improved
SimplePerformantInstrumentation class. It is designed to be more
performant and reduce object allocations.
/**
* This is invoked when the instrumentation step is initially
* dispatched
*
* @param result the result of the step as a completable future
*/
void onDispatched( CompletableFuture< T> result);
/**
* This is invoked when the instrumentation step is fully comple
*
* @param result the result of the step (which may be null)
* @param t this exception will be non-null if an exception
* was thrown during the step
*/
void onCompleted( T result, Throwable t);
InstrumentationState
Let’s discuss how state is managed in instrumentation.
@Component
class FieldCountInstrumentation
extends SimpleInstrumentation {
@Override
public InstrumentationState createState(
InstrumentationCreateStateParameters parameters) {
return new FieldCountState();
}
@Override
public InstrumentationContext< ExecutionResult> beginField(
InstrumentationFieldParameters parameters,
InstrumentationState state) {
(( FieldCountState) state).counter.incrementAndGet();
return noOp();
}
@Override
public InstrumentationContext< ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
return new InstrumentationContext< ExecutionResult>() {
@Override
public void onDispatched(
CompletableFuture< ExecutionResult> result) {
}
@Override
public void onCompleted( ExecutionResult result, Throwable t)
System.out.println(
"finished with " +
(( FieldCountState) state).counter.get() +
" Fields called"
);
);
}
};
}
}
ChainedInstrumentation
Spring for GraphQL automatically chains all detected
instrumentation beans. No further configuration is required.
ChainedInstrumentation chainedInstrumentation
= new ChainedInstrumentation( chainedList);
Built-in instrumentations
For convenience, GraphQL Java contains built-in instrumentations.
Name
DataLoaderDispatcher For DataLoader.
Instrumentation
ExecutorInstrumentation Controls on which thread calls
to DataFetchers happen on
FieldValidationInstrumentation Validates fields and their
arguments before query
execution. If errors are
returned, execution is
aborted.
MaxQueryComplexity Prevents execution of very
Instrumentation complex operations.
MaxQueryDepthInstrumentation Prevents execution of very
large operations.
TracingInstrumentation Implements the Apollo
Tracing format.
Step Description
Step Description
beginExecution Called when the overall execution
is started
beginParse Called when parsing of the
provided document string is
started
beginValidation Called when validation of the
parsed document is started
beginExecuteOperation Called when the actual operation
is being executed (meaning a
DataFetcher is invoked)
beginSubscribedFieldEvent Called when the subscription
starts (only for subscription
operations)
beginField Called for each field of the
operation
beginFieldFetch Called when the DataFetcher for a
field is called
beginFieldComplete Called when the result of a
DataFetcher is being processed
instrumentExecutionInput Allows for changing the
ExecutionInput
instrumentDocument Allows for changing the parsed
AndVariables document and/or the variables
instrumentSchema Allows for changing the
GraphQLSchema
instrumentExecutionContext Allows for changing the
ExecutionContext class that is
used by GraphQL Java internally
during execution.
Step Description
instrumentDataFetcher Allows for changing a DataFetcher
right before it is invoked
instrumentExecutionResult Allows for changing the overall
execution result
Let’s explain the n+1 problem with a simple example, people, and
their best friends.
type Query {
people: [Person]
}
type Person {
name: String
bestFriend: Person
}
@Controller
record PersonController( PersonService personService) {
@QueryMapping
List< Person> people() {
return personService.getAllPeople();
}
@SchemaMapping
Person bestFriend( Person person) {
return personService.getPersonById( person.bestFriendId());
}
}
While this code works, it will not perform well with large lists. For
every person in the list, we invoke the DataFetcher for the best
friend. For “n” people, we now have “n+1” service calls: one for
loading the initial list of people and then one for each of the n
people to load their best friend. This is where the name “n+1
problem” comes from. This can cause significant performance
problems as large lists will require many calls to retrieve data.
@Controller
record PersonController( PersonService personService) {
@QueryMapping
List< Person> people() {
return personService.getAllPeople();
}
// Usage
CompletableFuture< User> user1CF = userLoader.load( 1);
CompletableFuture< User> user2CF = userLoader.load( 2);
userLoader.dispatchAndJoin();
// Retrieve loaded users
User user1 = user1CF.get();
User user2 = user2CF.get();
Then we want to batch load the users. This dispatch step is triggered
with DataLoader.dispatchAndJoin(). This is a manual way to tell the
DataLoader instance that it is time to commence batch loading.
Note that in later examples, the dispatch point will be managed by
GraphQL Java.
Let’s continue with our example of people and their best friends.
This is how DataLoader works with the bestFriend DataFetcher, in
pure GraphQL Java.
import graphql. ExecutionInput;
import graphql. ExecutionResult;
import graphql. GraphQL;
import graphql. schema. DataFetcher;
import graphql. schema. GraphQLSchema;
import graphql. schema. idl. RuntimeWiring;
import graphql. schema. idl. SchemaGenerator;
import graphql. schema. idl. SchemaParser;
import graphql. schema. idl. TypeDefinitionRegistry;
import graphql. schema. idl. TypeRuntimeWiring;
import myservice. service. Person;
import myservice. service. PersonService;
import org. dataloader. BatchLoader;
import org. dataloader. DataLoader;
import org. dataloader. DataLoaderFactory;
import org. dataloader. DataLoaderRegistry;
type Person {
name: String
bestFriend: Person
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse( s
// Per request:
// Execute query
GraphQL graphQL = GraphQL.newGraphQL( schema).build();
ExecutionResult executionResult = graphQL.execute( executionInpu
}
}
The Person DataLoader is registered with the bestFriend
DataFetcher.
@Controller
class PersonController {
PersonService personService;
PersonController(
PersonService personService,
BatchLoaderRegistry batchLoaderRegistry) {
this.personService = personService;
@QueryMapping
List< Person> people() {
return personService.getAllPeople();
}
// Manually using DataLoader instance in DataFetcher
@SchemaMapping
CompletableFuture< Person> bestFriend(
Person person, DataLoader< Integer, Person> dataLoader) {
// Using the DataLoader
return dataLoader.load( person.bestFriendId());
}
@Controller
class PersonController {
PersonService personService;
// This constructor is written to emphasize
// Spring for GraphQL automation.
// You can instead use a record class
// and use the generated constructor.
PersonController( PersonService personService) {
this.personService = personService;
// No longer require BatchLoaderRegistry
// nor manual BatchLoader registration
}
@QueryMapping
List< Person> people() {
return personService.getAllPeople();
}
Argument Description
List<T> The list of source objects
java.security.Principal Spring Security principal
@ContextValue(name = A specific value from the
“foo”) GraphQLContext
GraphQLContext The entire GraphQLContext
BatchLoaderEnvironment org.dataloader.BatchLoaderWithContext
from DataLoader itself
In this chapter, we covered the n+1 problem when too many service
calls are used to fetch data. We solved the problem with
DataLoader, which is conveniently made available with
@BatchMapping in Spring for GraphQL. We then had a closer look at
how DataLoader works under the hood.
Testing
Spring for GraphQL provides helpers for GraphQL testing in a
dedicated artifact org.springframework.graphql:spring-graphql-test.
Testing a GraphQL service can happen on multiple levels, with
different scopes. In this chapter, we will discuss how Spring for
GraphQL makes it easier to write tests. At the end of the chapter,
we’ll conclude with our recommendations for writing good tests.
A simple query:
query greeting {
hello
}
@Controller
class GreetingController {
@QueryMapping
String hello() {
return "Hello, world!";
}
}
@ExtendWith( MockitoExtension.class)
class GreetingControllerTest {
@Test
void testHelloDataFetcher() {
GreetingController greetingController = new GreetingController
GraphQlTester
GraphQlTester is the primary class to help us test in Spring for
GraphQL. GraphQlTester is a Java interface with a few inner
interfaces, which provides a rich API to execute requests and verify
responses. There are a number of implementations for different
types of tests:
For example, here is a query for our Hello World example from
earlier in this chapter:
query greeting {
hello
}
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingControllerTest {
@Autowired
HttpGraphQlTester graphQlTester;
@Test
void usingTester() {
graphQlTester
.document( "query greeting { hello }")
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
}
}
We’ll soon explain all the parts in this test, but let’s start by
focusing on the GraphQlTester. We provide a document with document,
execute it, select a specific part of the response to verify with path,
and finally verify it is the string “Hello, world!”.
To make testing code more compact, note that the document in this
example is provided on a single line. This is equivalent to a query
with new lines, because new lines and additional whitespace are
ignored in GraphQL syntax.
We’ll see how this GraphQlTester fits into a test class in multiple
examples later in this chapter.
document or documentName
A document is provided with document.
Then we could rewrite our earlier test with documentName to use this
resource file containing the document:
graphQlTester
.documentName( "greeting")
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
type Pet {
name: String
}
@Controller
class PetsController {
@QueryMapping
Pet favoritePet() {
// return favorite pet from database
}
See the more complicated Pets example later in this chapter for
usage of entityList when a list of Pets is returned.
errors
By default, a GraphQL error in the response will not cause the test
to fail since a partial response in GraphQL is still a valid answer. See
why partial responses and nullable fields are valuable in the Schema
Design chapter.
graphQlTester
.document( "query greeting { hello }")
.execute()
.errors()
.verify() // Ensure there are no GraphQL errors
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
Note that the next few sections focus on HTTP where tests include
transport. For WebSocket tests, see subscriptions testing section
later in this chapter.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class E2ETest {
@Autowired
HttpGraphQlTester graphQlTester;
@Test
void testHello() {
String document = "query greeting { hello }";
graphQlTester.document( document)
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
}
}
This tests a whole GraphQL service over HTTP, verifying that the
request query greeting { hello } returns “Hello, world!”.
Application test
To test the whole service, without the HTTP transport layer, we can
start the whole application in the same Java Virtual Machine (JVM).
@Test
void testHello() {
String document = "query greeting { hello }";
graphQlTester.document( document)
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
}
This test only verifies the request inside the application, inside the
JVM. It is different to the previous end-to-end test, as the request in
this test does not go through the HTTP transport layer.
WebGraphQlHandler test
A WebGraphQlHandler test enables direct testing of WebGraphQlHandler.
This includes WebGraphQlInterceptor, because the WebGraphQlHandler
manages interceptors. Create a new tester instance by providing the
relevant WebGraphQlHandler.
package myservice. service;
@Autowired
WebGraphQlHandler webGraphQlHandler;
p Q p Q ;
@Test
void testHello() {
WebGraphQlTester webGraphQlTester
= WebGraphQlTester.create( webGraphQlHandler);
String document = "query greeting { hello }";
webGraphQlTester.document( document)
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
}
}
ExecutionGraphQlService test
ExecutionGraphQlServiceTester enables direct testing of
ExecutionGraphQlService. Create a new tester instance by providing
the relevant ExecutionGraphQlService.
package myservice. service;
@Autowired
ExecutionGraphQlService graphQlService;
@Test
void testHello() {
ExecutionGraphQlServiceTester graphQlServiceTester
= ExecutionGraphQlServiceTester.create( graphQlService);
String document = "query greeting { hello }";
g q y g g { } ;
graphQlServiceTester.document( document)
.execute()
.path( "hello")
.entity( String.class)
.isEqualTo( "Hello, world!");
}
type Pet {
name: String
}
@Controller
record PetsController( PetService petService) {
@QueryMapping
List< Pet> pets() {
return petService.getPets();
}
The controller uses a Pet service, which fetches a list of Pets from a
data source, which could be a database or another service, or
anything else.
package myservice. service;
@Service
class PetService {
@GraphQlTest( PetsController.class)
class PetsControllerTest {
@Autowired
GraphQlTester graphQlTester;
@MockBean
PetService petService;
@Test
void testPets() {
Mockito.when( petService.getPets())
.thenReturn( List.of(
new Pet( "Luna"),
new Pet( "Skipper")
));
graphQlTester
.document( "query myPets { pets { name } }")
.execute()
.path( "pets[*].name")
.entityList( String.class)
.isEqualTo( List.of( "Luna", "Skipper"));
}
}
As an alternative, you could verify there were at least two pet names
by replacing the last block of the test above with:
graphQlTester
.document( "query myPets { pets { name } }")
.execute()
.path( "pets[*].name")
.entityList( String.class)
.hasSizeGreaterThan( 2);
In these examples, the path for Pets was more complex than our
Hello World example. "pets[*].name" means select all names of all
pets. We can use any JsonPath with path.
Subscription testing
GraphQlTester offers an executeSubscription method that returns a
GraphQlTester.Subscription. This can be then further converted to a
Flux and verified. To test Flux more easily, add the Reactor testing
library io.projectreactor:reactor-test.
type Subscription {
hello: String
}
@Controller
class HelloController {
@SubscriptionMapping
Flux< String> hello() {
Flux< Integer> interval = Flux.fromIterable( List.of( 0, 1, 2))
.delayElements( Duration.ofSeconds( 1));
return interval.map( integer -> "Hello " + integer);
}
}
spring.graphql.websocket.path=/graphql
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SubscriptionTest {
@Value( "http://localhost:${local.server.port}"
+ "${spring.graphql.websocket.path}")
private String baseUrl;
GraphQlTester graphQlTester;
@BeforeEach
void setUp() {
URI url = URI.create( baseUrl);
this.graphQlTester = WebSocketGraphQlTester.builder(
url, new ReactorNettyWebSocketClient()
).build();
}
@Test
void helloSubscription() {
Flux< String> hello = graphQlTester
.document( "subscription mySubscription {hello}")
.executeSubscription()
.toFlux( "hello", String.class);
StepVerifier.create( hello)
.expectNext( "Hello 0")
.expectNext( "Hello 1")
.expectNext( "Hello 2")
.verifyComplete();
}
}
The same way we have tested subscription end to end here also
allows us to test subscriptions on different layers.
Testing recommendations
A general guide for writing good tests is to have the smallest or
most focused test possible that verifies what we want to test.
These testing guidelines fit into the Test Pyramid model. The idea is
to have more of focused, smaller and faster tests, compared to the
number of tests that run longer, test more aspects, and are harder to
debug. This model gives us some guidance about the amount of
tests per test type. It is preferable to have more unit tests than
WebGraphQlHandlerTests, and it is preferable to have more
WebGraphQlHandlerTests than the number of end-to-end tests.
As security is not part of the GraphQL Java engine, this chapter will
instead focus on using Spring for GraphQL and Spring Security to
secure your GraphQL service. Spring for GraphQL has built-in,
dedicated support for Spring Security.
type Order {
id: ID
details: String
}
type Mutation {
# Only Admins can delete orders
deleteOrder(input : DeleteOrderInput!): DeleteOrderPayload
}
input DeleteOrderInput {
orderId: ID
}
type DeleteOrderPayload {
success: Boolean
}
Let’s implement a very simple Java class OrderService that loads and
changes orders, which are stored in memory.
package myservice. service;
@Service
class OrderService {
OrderService() {
// A mutable list of orders
this.orders = new ArrayList<>( List.of(
new Order( "1", "Kibbles", "Luna"),
new Order( "2", "Chicken", "Skipper"),
new Order( "3", "Rice", "Luna"),
new Order( "4", "Lamb", "Skipper"),
new Order( "5", "Bone", "Luna"),
new Order( "6", "Toys", "Luna"),
new Order( "7", "Toys", "Skipper")
));
}
org.springframework.boot:spring-boot-starter-security
@Configuration
@EnableWebFluxSecurity
class Config {
@Bean
SecurityWebFilterChain springWebFilterChain( ServerHttpSecurity h
throws Exception {
http.formLogin().authenticationSuccessHandler(
new RedirectServerAuthenticationSuccessHandler( "/graphiql
);
return http
.csrf( ServerHttpSecurity.CsrfSpec:: disable)
.authorizeExchange( exchanges -> {
exchanges.anyExchange().authenticated();
})
.build();
}
@Bean
@SuppressWarnings( "deprecation")
MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncod
UserDetails luna = userBuilder
.username( "Luna").password( "password").roles( "USER")
.build();
UserDetails andi = userBuilder
.username( "Andi").password( "password").roles( "USER", "ADMIN
.build();
return new MapReactiveUserDetailsService( luna, andi);
}
}
@Controller
record OrderController( OrderService orderService) {
@QueryMapping
List< Order> myOrders( Principal principal) {
return orderService.getOrdersByOwner( principal.getName());
}
.Argument;
import org. springframework. graphql. data. method. annotation
.MutationMapping;
import org. springframework. graphql. data. method. annotation
.QueryMapping;
import org. springframework. security. access
.AccessDeniedException;
import org. springframework. security. authentication
.UsernamePasswordAuthenticationToken;
import org. springframework. security. core. authority
.SimpleGrantedAuthority;
import org. springframework. stereotype. Controller;
@Controller
record OrderController( OrderService orderService) {
@QueryMapping
List< Order> myOrders( Principal principal) {
return orderService.getOrdersByOwner( principal.getName());
}
@MutationMapping
DeleteOrderPayload deleteOrder( @Argument DeleteOrderInput input,
Principal principal) {
UsernamePasswordAuthenticationToken user
= ( UsernamePasswordAuthenticationToken) principal;
if (! user.getAuthorities()
.contains(new SimpleGrantedAuthority( "ROLE_ADMIN"))) {
throw new AccessDeniedException( "Only admins can delete ord
}
return new DeleteOrderPayload( orderService
.deleteOrder( input.orderId()));
}
}
We use the injected Principal again, but this time we use it to verify
that the current user has the correct role, rather than filtering
orders. If the user is unauthorized, we throw an
AccessDeniedException.
After we log out via /logout and login as “Andi” (with password
“password”), we can delete an order.
Method security
One problem with our store order example above is the location
where we perform the authorization checks. They happen directly
inside each DataFetcher. This is not great. The better and
recommended way is to secure the OrderService itself, so that it is
secure, regardless which DataFetcher uses it.
@PreAuthorize( "hasRole('ADMIN')")
Mono< Boolean> deleteOrder( String orderId) {
Putting it all together, here is the full source code for Config,
Controller, and OrderService.
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class Config {
@Bean
@Bean
SecurityWebFilterChain springWebFilterChain(
ServerHttpSecurity http
) throws Exception {
http
.formLogin()
.authenticationSuccessHandler(
new RedirectServerAuthenticationSuccessHandler( "/graphi
);
return http
.csrf( ServerHttpSecurity.CsrfSpec:: disable)
.authorizeExchange( exchanges -> {
exchanges.anyExchange().authenticated();
})
.build();
}
@Bean
@SuppressWarnings( "deprecation")
MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPasswordEncod
UserDetails luna = userBuilder
.username( "Luna").password( "password")
.roles( "USER").build();
UserDetails andi = userBuilder
.username( "Andi").password( "password")
.roles( "USER", "ADMIN").build();
return new MapReactiveUserDetailsService( luna, andi);
}
}
@QueryMapping
Mono< List< Order>> myOrders() {
return orderService.getOrdersForCurrentUser();
}
@MutationMapping
Mono< DeleteOrderPayload> deleteOrder(
@Argument DeleteOrderInput input) {
Mono< Boolean> booleanMono = orderService
.deleteOrder( input.orderId());
return booleanMono.map( DeleteOrderPayload::new);
}
@Service
class OrderService {
OrderService() {
// A mutable list of orders
this.orders = new ArrayList<>( List.of(
new Order( "1", "Kibbles", "Luna"),
new Order( "2", "Chicken", "Skipper"),
new Order( "3", "Rice", "Luna"),
new Order( "4", "Lamb", "Skipper"),
new Order( "5", "Bone", "Luna"),
new Order( "6", "Toys", "Luna"),
new Order( "7", "Toys", "Skipper")
));
}
@PreAuthorize( "hasRole('ADMIN')")
Mono< Boolean> deleteOrder( String orderId) {
return Mono.just( orders
.removeIf( order -> order.id().equals( orderId)));
}
Testing auth
For an introduction to testing, please see the previous chapter on
Testing.
Let’s write end-to-end auth tests. To start with the simplest test and
establish a good baseline, let’s verify that we reject unauthenticated
requests.
package myservice. service;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class AuthE2ETest {
@Autowired
WebTestClient webTestClient;
@Test
void shouldRejectUnauthenticated() {
String document = "query orders { myOrders { id } }";
Map< String, String> body = Map.of( "query", document);
webTestClient
.mutateWith(
( builder, httpHandlerBuilder, connector)
-> builder.baseUrl( "/graphql"))
.post()
.contentType( MediaType.APPLICATION_JSON)
.accept( MediaType.APPLICATION_JSON)
.bodyValue( body)
.exchange()
.expectStatus().isEqualTo( HttpStatus.FOUND);
}
}
In this end-to-end test, we reject a GraphQL request with a 302
Found result, and redirect to another page (the login page).
Depending on the service, we could assert another HTTP status code
such as 401 Unauthorized.
If you prefer to test only the GraphQL layer, rather than the whole
service end-to-end, you can use WebGraphQlTester.
Let’s test that we return the correct orders for the authenticated
user.
package myservice. service;
@Autowired
WebGraphQlHandler webGraphQlHandler;
@Component
static class WebInterceptor implements WebGraphQlInterceptor {
@Override
public Mono< WebGraphQlResponse> intercept( WebGraphQlRequest re
Chain chain) {
UsernamePasswordAuthenticationToken authenticated =
UsernamePasswordAuthenticationToken.authenticated(
"Luna", "password",
List.of(new SimpleGrantedAuthority( "ROLE_USER")));
@Test
void testCorrectOrdersAreReturned() {
WebGraphQlTester webGraphQlTester
= WebGraphQlTester.create( webGraphQlHandler);
String document = "query orders { myOrders { id } }";
webGraphQlTester.document( document)
.execute()
.errors()
.verify()
.path( "myOrders[*].id")
.entityList( String.class)
.isEqualTo( List.of( "1", "3", "5", "6"));
// Luna's orders previously defined in the OrderService
}
}
Within the same class, we can also test that an unauthorized user
cannot delete orders.
@Test
void testMutationForbidden() {
WebGraphQlTester webGraphQlTester
= WebGraphQlTester.create( webGraphQlHandler);
String document = """
mutation delete( $id: ID){
deleteOrder( input:{ orderId: $id}){ success}} """;
webGraphQlTester.document( document)
.variable( "id", "1")
.execute()
.errors()
.expect( responseError ->
responseError.getMessage().equals( "Forbidden") &&
responseError.getPath().equals( "deleteOrder")).verify()
}
Note how this test verifies a GraphQL error, not an HTTP status
code, because the overall HTTP response is a 200. We verify that the
message and the path match our expectation.
HTTP client
The HTTP GraphQL client is basically a wrapper around a WebClient,
so we need to provide a WebClient when creating a HttpGraphQlClient.
package myservice. service;
or
HttpGraphQlClient graphQlClient = HttpGraphQlClient
.builder( webClient)
.headers( httpHeaders -> httpHeaders.setBearerAuth( "token"))
.build();
WebSocket client
The WebSocketGraphQlClient uses a WebSocketClient under the hood.
We have to provide a WebSocketClient when creating a new
WebSocketGraphQlClient. Note that WebSocketClient is an abstraction
with implementations for Reactor Netty, Tomcat and others.
Note that in the WebSocket client we provide the URL via the
builder builder(url, client), whereas in the HTTP client it is set via
the builder url.
GraphQlClient
We can only use GraphQlClient after creating an instance of an HTTP
or WebSocket client. In the following examples, graphQlClient could
be either an HTTP or WebSocket client.