Graphql Java
Graphql Java
Prologue i
Andi (Andreas) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
Donna . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
About this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii
Introduction 1
Your first Spring for GraphQL service . . . . . . . . . . . . . . . . . . . . . . . 1
What is GraphQL? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
A brief history of GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
From GraphQL Java to Spring for GraphQL . . . . . . . . . . . . . . . . . . . 12
Overview 14
Three layers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Schema and SDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
GraphQL query language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Request and Response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Execution and DataFetcher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
How concepts relate to each other . . . . . . . . . . . . . . . . . . . . . . . . . 19
GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Schema 22
Schema-first . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Loading schema resources in Spring for GraphQL . . . . . . . . . . . . . . . . . 22
GraphQL schema elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
GraphQL types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Fields everywhere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Scalar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Input object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
List and NonNull . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
ii
Contents
DataFetchers 47
Spring for GraphQL annotated methods . . . . . . . . . . . . . . . . . . . . . . 48
PropertyDataFetchers in Spring for GraphQL . . . . . . . . . . . . . . . . . . . 50
DataFetchers and schema mapping handler methods . . . . . . . . . . . . . . . 52
TypeResolver in Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 52
Arguments in Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . 56
More Spring for GraphQL inputs . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Adding custom scalars in Spring for GraphQL . . . . . . . . . . . . . . . . . . . 59
Under the hood: DataFetchers inside GraphQL Java . . . . . . . . . . . . . . . 61
DataFetchers in GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Source objects in GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . 64
RuntimeWiring in GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Creating an executable schema in GraphQL Java . . . . . . . . . . . . . . . . . 65
TypeResolver in GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
iii
Contents
GraphQL arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Unions, interfaces, and TypeResolver . . . . . . . . . . . . . . . . . . . . . . . . 86
Subscriptions 91
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Client support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
iv
Contents
Directives 154
Schema and operation directives . . . . . . . . . . . . . . . . . . . . . . . . . . 154
Built-in directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
@skip and @include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
@deprecated . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
@specifiedBy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Defining your own schema and operation directives . . . . . . . . . . . . . . . . 159
Defining schema directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
Defining operation directives . . . . . . . . . . . . . . . . . . . . . . . . . 161
Repeatable directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Implementing logic for schema directives . . . . . . . . . . . . . . . . . . . . . . 163
Changing execution logic with schema directives . . . . . . . . . . . . . . 163
Validation with schema directives . . . . . . . . . . . . . . . . . . . . . . . 165
Adding metadata with schema directives . . . . . . . . . . . . . . . . . . . 167
Implementing logic for operation directives . . . . . . . . . . . . . . . . . . . . 170
Execution 173
Initializing execution objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
How Spring for GraphQL starts execution . . . . . . . . . . . . . . . . . . . . . 174
Execution steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Parsing and validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Coercing variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Fetching data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Reactive concurrency-agnostic . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179
Completing a field . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
TypeResolver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Query vs mutation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Instrumentation 184
Instrumentation in Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . 184
Writing a custom instrumentation . . . . . . . . . . . . . . . . . . . . . . . . . 185
InstrumentationContext . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
InstrumentationState . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
ChainedInstrumentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Built-in instrumentations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
List of instrumentation hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
v
Contents
DataLoader 193
The n+1 problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Solving the n+1 problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
DataLoader overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
DataLoader and GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
DataLoader and Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 201
@BatchMapping method signature . . . . . . . . . . . . . . . . . . . . . . . . . . 204
Testing 205
Unit testing DataFetcher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
GraphQlTester . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
document or documentName . . . . . . . . . . . . . . . . . . . . . . . . . . 208
GraphQlTester.Request and execute . . . . . . . . . . . . . . . . . . . . 209
GraphQlTester.Response, path, entity, entityList . . . . . . . . . . . 209
errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Testing different layers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
End-to-end over HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Application test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
WebGraphQlHandler test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
ExecutionGraphQlService test . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Focused GraphQL testing with @GraphQlTest . . . . . . . . . . . . . . . . . . . 216
Subscription testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
Testing recommendations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Security 223
Securing a Spring for GraphQL service . . . . . . . . . . . . . . . . . . . . . . . 223
Spring for GraphQL support for security . . . . . . . . . . . . . . . . . . . . . . 231
Method security . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Testing auth . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
vi
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.
After looking into GraphQL, it immediately convinced me of the value it could provide. Of
course, I could not predict its success, but I experienced firsthand in multiple companies
the struggles of creating and maintaining a REST API. Despite serious effort, they all
looked more like GraphQL over time rather than how a good REST API was supposed
to look. Perhaps it came from a lack of understanding REST, but after I had witnessed
very similar challenges again and again in very different contexts, I thought otherwise.
Being convinced of the value, I started immediately working on a Java implementation
after they released the GraphQL specification. After about two weeks of investing all the
free time I had outside my job, I released the first version of GraphQL Java.
While it was just me in the beginning, shortly after I received the first PR that fixed
a typo. Today more than 200 people1 have contributed to GraphQL Java and without
them there would be no GraphQL Java. Sincerely, thanks a lot to all of you.
Many thanks and a special mention belongs to Brad Baker2 , 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.
1
https://github.com/graphql-java/graphql-java/graphs/contributors
2
https://github.com/bbakerman
i
Prologue
I discovered programming later in life, initially it was only a hobby. One of the first
programming ideas I learned about was open source software. As a profit-maximising
investment banker, it seemed delightfully nuts that high quality work could be happily
given away for free. I adored the spirit of collaboration and community in the open source
world. I adored it so much that I changed careers and became a software engineer.
Thanks to everyone in the GraphQL Java community for your contributions over the
years.
This book is for anyone who wants to build a production GraphQL service with Java. By
the end of this book, you will be confident building your own production GraphQL service
with Spring for GraphQL, the official Spring integration built on top of the GraphQL
Java engine. Spring for GraphQL makes it easier than ever to build a GraphQL service
by eliminating boilerplate code and seamlessly integrating with the Spring ecosystem.
GraphQL Java is the dominant Java implementation of GraphQL, powering services at
Twitter, AirBnB, Netflix, Atlassian, and many other companies. By the end of this book,
you’ll be leveraging the same engine with Spring for GraphQL.
In this book, you’ll learn key GraphQL concepts, paired with practical advice from our
experiences running production GraphQL services at scale. At the end of this book,
you’ll have in depth knowledge of Spring for GraphQL and the GraphQL Java engine, so
you will have the confidence to run production ready GraphQL services.
This book is suitable for beginners building their first production GraphQL service. There
are also advanced topics later in the book for intermediate readers.
We do not assume any prior knowledge of GraphQL. To make the most of this book,
we assume basic Java knowledge, and we assume very basic Spring3 knowledge such as
familiarity with the @Component annotation. Optionally, if you intend to build a reactive
service, you should be familiar with the Reactor concepts of Mono and Flux.
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.
If you have feedback or comments on the book, please let us know via email at book-
feedback@graphql-java.com.
We deeply hope you enjoy this book.
Special thanks to our reviewers for giving us fantastic feedback. Thanks to Rossen
Stoyanchev and Brian Clozel from the Spring for GraphQL team, our technical reviewer
3
https://spring.io/
ii
Prologue
Doug Warren, our reviewers Brad Baker, Antoine Boyer, Felipe Reis, Stephan Behnke,
and Josh Long. Thanks to our cover designer Mike Riethmuller.
iii
Introduction
The best way to get a feeling for GraphQL is to experience it. We will walk through how
to create your first Spring for GraphQL service, step by step. In the coming chapters, we
will explain these steps in greater detail.
First, create a new project with Spring Initializr1 , at https://start.spring.io. In this book,
we will use Spring Boot 3.x which requires at least Java 17. If you prefer to use Java 11,
select a Spring Boot version of at least 2.7.0 to use Spring for GraphQL. You can choose
between a Maven or Gradle project.
In the dependencies section, add Spring for GraphQL. We’ll then need to add one
more dependency for underlying transport. You can choose either Spring Reactive Web
(WebFlux) or Spring Web (Spring MVC). In this book, we’ll be using Spring Reactive
Web to make use of the WebFlux framework and the Netty server for reactive services.
If you choose Spring Web (which includes Spring MVC), all content in this book is still
applicable to your service, as Spring for GraphQL fully supports both Spring MVC and
WebFlux. All examples in this book are almost identical for Spring MVC, the only
difference is that controller methods will not be wrapped in a Mono or Flux.
You can add your own project metadata, or follow along with our example in the
screenshot in Figure 1.
Click Generate at the bottom of the page to generate your project. Open the
project in your favourite code editor. Start the application from the main method
in myservice.service.ServiceApplication. It will start an HTTP endpoint at
http://localhost:8080, but not much else yet!
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.
1
https://start.spring.io
1
Introduction
2
Introduction
Let’s start by creating a GraphQL schema, which is a static description of the API.
Create a new subdirectory “graphql” under “resources”. Paste the following into a new
file src/main/resources/graphql/schema.graphqls.
type Query {
pets: [Pet]
}
type Pet {
name: String
color: String
}
This GraphQL schema for pets is written in Schema Definition Language (SDL) format.
This defines a GraphQL query type with a pets field, which returns a list of pets. Each
pet has a name and color, and both fields are string attributes. In the Schema chapter,
we’ll discuss GraphQL schema elements in depth.
Add a Pet record class in the package myservice.service.
package myservice.service;
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.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class PetsController {
@QueryMapping
List<Pet> pets() {
return List.of(
new Pet("Luna", "cappuccino"),
new Pet("Skipper", "black"));
3
Introduction
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.
To add a visual frontend to explore our API, enable the built-in GraphiQL2 in-
teractive playground by adding spring.graphql.graphiql.enabled=true to the
application.properties file in src/main/resources. GraphiQL is a small web app
for exploring GraphQL APIs interactively from the browser. Think of it as a REPL
(“Read-Eval-Print Loop”) tool for GraphQL.
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.
If your service is not starting correctly, double check that your schema file
schema.graphqls is stored in the graphql subfolder inside resources.
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 Figure 2.
On the left-hand side of the GraphiQL playground, we see our query and on the right we
see the JSON response. The structure of the response matches the query exactly.
Take a moment to experiment with GraphiQL. For example, you could remove color
from the query. Try the automatic completion help when typing a query.
Open the Documentation Explorer by clicking on document icon in the top left corner,
as shown in Figure 3.
2
https://github.com/graphql/graphiql
4
Introduction
Figure 2: GraphiQL
5
Introduction
Then click on the root Query for pets, and then click on Pet to see its attributes (name
and color), as shown in Figure 4.
Congratulations on completing your first Spring for GraphQL service! In this chapter,
we discussed concepts at a very high level. In the remainder of this book, we’ll discuss
each concept in greater detail. Later, in the Building a GraphQL service chapter, we’ll
extend this service with more features.
To quickly recap what we did:
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 Figure 5.
We sometimes describe the two parties involved as the API “consumer” and “producer”.
In practical terms, GraphQL comprises two different parts: a domain-specific language
that enables the client to specify their intent such as what data to query, which action
to perform, data attributes to be returned, and a backend service able to execute this
request. The domain-specific language is called the “GraphQL query language”; although
“query” unfortunately is an overloaded term, since the “query language” lets you perform
updates (or “mutations”), subscriptions, and queries.
6
Introduction
# This is a comment
query myPets {
pets {
name
}
}
This is a “query operation”, but there are also “mutation” and “subscription” operations
that can be executed by a GraphQL service. We’ll explain operations in more detail in
the Query Language chapter. Assuming there are two pets named Luna and Skipper,
this is the JSON response:
{
"data":
{
"pets": [
{
"name": "Luna"
},
{
"name": "Skipper"
}
]
}
}
7
Introduction
This also shows a key feature of GraphQL: we need to explicitly ask for every piece of
information we want. The response contains exactly the fields we asked for: the “name”
of every pet.
If we changed the query and also ask for the color:
query myPets {
pets {
name
color
}
}
{
"pets": [
{
"name": "Luna",
"color": "cappuccino"
},
{
"name": "Skipper",
"color": "black"
}
]
}
Technically speaking, we normally send a GraphQL request as an HTTP POST with the
operation specified in the HTTP body with the response as JSON. Every POST request
is sent to the same URL (typically ending with /graphql), but the body of the POST
request varies based on what you want to request.
The GraphQL ecosystem today offers implementations in nearly every language3 . The
GraphQL specification4 (often shortened to “spec”) maintains consistency across all
implementations, which defines the exact behaviour of each GraphQL request. Beside
the spec, there is a reference implementation5 written in TypeScript.
One thing to note is that the spec describes how to execute a GraphQL request should
be executed with no considerations of the transport layer. A GraphQL request (in the
abstract spec sense) could be an in-memory API call or could be a request via RSocket6 ,
3
https://graphql.org/code/
4
https://graphql.org/code/
5
https://github.com/graphql/graphql-js
6
https://rsocket.io/
8
Introduction
the spec describes merely a “GraphQL engine”. This is important for later to understand
for how GraphQL Java and Spring for GraphQL relate to each other.
While in theory GraphQL could be executed over many transport protocols, the vast
majority of GraphQL APIs use HTTP. Although the spec does not yet specify GraphQL
over HTTP, the GraphQL community in practice agrees on a GraphQL request over
HTTP standard. In this book, we will also only focus on GraphQL via HTTP for queries
and mutations, and GraphQL via WebSocket for subscriptions.
A GraphQL API is a statically typed API. “Typed” means that a GraphQL API contains
a clear description of what the consumers can do with an API. The API doesn’t change
often. When the API does change, it normally involves a redeployment of the service.
For example, the API used by the queries above would look like this:
# This is a comment
This syntax is called Schema Definition Language (SDL) and the structure of a GraphQL
API is called a schema. Every GraphQL API has a schema that clearly describes the
API in SDL syntax. The best way to think about a GraphQL API for now is that it is a
list of types with a list of fields.
Every GraphQL API offers special fields that let you query the schema of the API itself.
This feature is called introspection. For example, a valid query for every GraphQL API
is this:
query myIntrospection {
__schema {
types {
name
}
}
}
9
Introduction
{
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "Pet"
}
]
}
}
As you can see the special field __schema starts with __, which indicates this is an
introspection field, not a normal field.
The official birthday of GraphQL was the 29th of February 2012, when it was an internal
proposal at Facebook with the name SuperGraph7 .
It was part of the effort to rewrite the Facebook iOS client as a native app and aimed to
solve multiple problems the team encountered with traditional REST like APIs described
by co-creator Lee Byron in this 10-minute video keynote of a Brief History of GraphQL8 :
• Slow on the network: multiple coordinated round-trips were required to fetch the
needed data.
7
https://twitter.com/leeb/status/1498759168689598464?s=20&t=817z2q0x1_x2En8xxH5X8w
8
https://www.youtube.com/watch?v=VjHWkBr3tjI
10
Introduction
• Fragile client/server relationship: changes to the server API could easily break the
app and docs were often out of date.
• Tedious code and process: necessary service changes often blocked client develop-
ment.
GraphQL addressed these issues with the features outlined in the previous section: a
query language allows the client to specify exactly what they want, develop flexibly
and independently of the server, and a static type system that makes the client/server
relationship much more stable.
After GraphQL was successfully used inside Facebook for a few years, it was open sourced
in July 20159 . Two artifacts were published together: the GraphQL spec and the reference
implementation. The reference implementation was initially a JavaScript implementation
of the spec, but it’s now also available in TypeScript. This dual approach of having a
clear spec together with a reference implementation led to implementations across every
major programming language and ecosystem, including Ruby, PHP, .NET, Python, Go,
and of course Java.
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 Foundation10 . The GraphQL Foundation
is a vendor-neutral entity, comprising over 25 members11 . The list includes AWS, Airbnb,
Atlassian, Microsoft, IBM, and Shopify. The official description of the foundation12 is:
Legally, the GraphQL Foundation owns the GraphQL trademark and the copyright for
certain GraphQL projects.
The official web page of GraphQL is https://graphql.org. Note that the domain ending
in .com (https://graphql.com) is an unrelated page owned by a company.
Practically speaking, the Foundation is ultimately responsible for the official GraphQL
projects under the GraphQL GitHub organization13 . It includes the spec, the reference
implementation, and GraphiQL. Most of the GraphQL implementations are not part of
the GraphQL Foundation, even though they implement the spec.
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
9
https://youtu.be/WQLzZf34FJ8?t=1473
10
https://medium.com/@leeb/introducing-the-graphql-foundation-3235d8186d6d
11
https://graphql.org/foundation/members/
12
https://graphql.org/foundation/
13
https://github.com/graphql
11
Introduction
the GraphQL community can join. More details are available in the working group
GitHub repository14 .
Shortly after GraphQL was open sourced, a first version of GraphQL Java15 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.
While I am still quite happy with this decision, because it allowed GraphQL Java to have
a strong focus and widespread adoption, it came with one clear downside: every service
would need to solve the HTTP integration itself.
So some time after I released GraphQL Java, the first GraphQL Java Spring integrations
became available. I even developed a small GraphQL Java Spring16 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 milestone17 and
after that, the first release of Spring for GraphQL18 in May 2022.
Spring for GraphQL aims to be an unopinionated integration of GraphQL Java into
Spring with a focus on comprehensive and wide-ranging support. It should serve as a
fundamental building block for GraphQL solutions with Spring. Another key design
decision was the direct usage of GraphQL Java itself, there is no abstraction or additional
layer between Spring and GraphQL Java.
Spring for GraphQL comprises two parts: one is the actual Spring Framework integration19
with GraphQL Java, and on top of that there is the Spring Boot Starter20 for GraphQL.
In this book, we will build services developed with Spring Boot that take advantage of
the Spring Boot GraphQL Starter.
We will constantly move between the GraphQL Java and Spring world, but it is often
important to understand which layer contributes what part in order to take full advantage
14
https://github.com/graphql/graphql-wg
15
https://github.com/graphql-java/graphql-java/releases/tag/v1.0
16
https://github.com/graphql-java/graphql-java-spring
17
https://spring.io/blog/2021/07/06/hello-spring-graphql
18
https://spring.io/projects/spring-graphql
19
https://docs.spring.io/spring-graphql/docs/current/reference/html/
20
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#web.graphql
12
Introduction
of Spring for GraphQL. This is especially valuable for troubleshooting. Throughout this
book, we’ll make it clear when we talk about a Spring for GraphQL concept and when
we are directly discussing a GraphQL Java concept.
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.
13
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 under-
standing and not get lost in the details in the next chapters.
This chapter will cover concepts at a high level to provide an overview. Later in the
book, we’ll explain these concepts in greater detail.
Three layers
We have three layers to consider, where the higher ones depend on the lower ones.
From bottom to top in Figure 1:
We can also look at this as a hierarchy of abstractions, from less opinionated at the
bottom to more specific at the top:
• The spec is the most abstract as it is relevant for all implementations of GraphQL,
not only for Java. It is only a text document, not running software. For example,
it describes what a GraphQL mutation is, without describing the details of its
implementation.
• GraphQL Java implements the spec and actually provides running software. It is
not tied to any specific framework like Spring and doesn’t deal with any transport
level details.
• Spring for GraphQL: leverages GraphQL Java to provide a comprehensive framework
for building GraphQL services, including transport level details.
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.
14
Overview
15
Overview
A GraphQL API uses static types, so it can only do what it clearly describes. Changing
the API usually involves a redeployment of our service.
The schema is the description of the API, which is an instance of GraphQLSchema in
GraphQL Java.
The Schema Definition Language (SDL) syntax defines the schema in a human-readable
format.
type Query {
pets: [Pet]
}
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.
We will discuss GraphQL schemas in depth in the Schema chapter.
The GraphQL query language is a domain-specific language for describing what a client
wants to request. It looks similar to JSON on purpose, but it is not JSON.
query myPets {
pets {
name
}
}
The client is required to explicitly “select” any data it wants, such as the names of pets.
The response is in JSON, and only contains the data requested, no more and no less.
16
Overview
{
"data":{
"pets": [
{
"name": "Luna"
},
{
"name": "Skipper"
}
]
}
}
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.
Every interaction of a consumer of a GraphQL API starts with a GraphQL request, and
results in a GraphQL response.
A GraphQL request in GraphQL Java is an instance of ExecutionInput and the response
is an ExecutionResult. These are request and response definitions independent of a
specific transport level. From a GraphQL Java perspective, a request and a response are
just an argument and return value of a method call. We’ll discuss GraphQL requests
and responses in more detail in a dedicated chapter later in the book.
Spring for GraphQL defines a GraphQlRequest with the most important implementation
being WebGraphQlRequest for requests via HTTP or WebSocket. WebGraphQlRequest
contains specific data for the HTTP request, such as URL and HTTP headers.
A response in Spring for GraphQL is a GraphQlResponse with WebGraphQlResponse
again being specific to HTTP or WebSocket, containing the HTTP response headers.
Most notably, a GraphQL request contains a GraphQL document, which is text in
GraphQL query language format that contains the intent of the client.
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.
17
Overview
We can roughly divide the actual execution of a GraphQL request via HTTP like this:
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.
Step two involves multiple steps, such as invoking the relevant DataFetchers that retrieve
the data necessary to fulfill the request. We’ll go into much more detail on DataFetchers
in a dedicated chapter later in this book.
In Spring for GraphQL, annotated controller methods are registered as DataFetchers.
For example, in the previous chapter we built a sample service with a list of pets:
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class PetsController {
@QueryMapping
List<Pet> pets() {
return List.of(
new Pet("Luna", "cappuccino"),
new Pet("Skipper", "black"));
}
The @QueryMapping annotation registers this method as a DataFetcher for a field pets in
the type Query, connecting the method to the field of the same name. We’ll explain Spring
for GraphQL’s schema mapping annotations in detail in the DataFetchers chapter.
The last step of creating and serializing the response is handled by Spring for GraphQL.
For more on execution, see the dedicated chapter later in this book.
18
Overview
As shown in Figure 2,
GraphQL Java
19
Overview
The primary classes of Spring for GraphQL are (all class name start with
org.springframework.graphql, abbreviated as o.s.g):
o.s.g.server.webflux.GraphQlHttpHandler: WebFlux handler. Creates WebGraphQlRequest
and calls a WebGraphQlHandler
o.s.g.server.webmvc.GraphQlHttpHandler: Spring MVC handler. Creates
WebGraphQlRequest and calls a WebGraphQlHandler
o.s.g.server.WebGraphQlRequest: A GraphQL request over HTTP or WebSocket.
o.s.g.server.WebGraphQlResponse: A GraphQL response over HTTP or Web-
Socket.
o.s.g.server.WebGraphQlHandler: Handles GraphQL requests over HTTP or Web-
Socket.
o.s.g.server.WebGraphQlInterceptor: Hook to intercept a WebGraphQlRequest and
WebGraphQlResponse.
o.s.g.graphql.ExecutionGraphQlService: The actual service responsible for invoking
GraphQL Java.
20
Overview
A request passes through three primary classes in Spring for GraphQL, each with a
distinct responsibility, as shown in Figure 4:
The most relevant of these steps in daily usage is the second one as the
WebGraphQlHandler offers the ability to intercept any request and response via
WebGraphQlInterceptor.
A quick note on capitalization. You might have noticed the capitalization of the letters
“Q” and “L” in class names vary. In all Spring for GraphQL classes, “Q” is always in upper
case and “l” is always in lower case, to be consistent with Spring naming conventions. In
GraphQL Java, names usually include “Q” and “L” in upper case. Where this convention
has not been followed, we’ll call it out in the relevant code example.
This chapter summarized the most fundamental concepts at a high level. Throughout
this book, we’ll expand on all these topics is greater detail.
21
Schema
The schema of a GraphQL API is the static description of the API. A schema describes
what a consumer of the API may request, which can be represented in Schema Definition
Language (SDL) format (also called “SDL syntax” or “SDL notation”).
Under the hood, Spring for GraphQL represents an executable schema with GraphQL
Java’s GraphQLSchema, which is then used to create a GraphQL object. It is an “executable”
schema because it contains all the logic needed to execute a request, as well as the
description of the API for the consumer. In this chapter, we will focus on the API
description. In the DataFetchers chapter, we will discuss how this API description
connects to the execution logic.
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.
Schemas ought to be schema-first because they should be created in a deliberate way
and not merely generated. Although a GraphQL API will have much in common with
the database schema or REST API being used to fetch data, the schema should still be
deliberately constructed.
We strongly believe that this is the only viable approach for any real-life GraphQL API
and we will only focus on this approach. Both Spring for GraphQL and GraphQL Java
only support “schema-first”.
In GraphQL Java, the schema is represented by an instance of GraphQLSchema. This
can be created either via SDL or programmatically. Both approaches are “schema-first”
because the schema is deliberately designed. In this book, all examples will use schemas
created via SDL.
Spring for GraphQL automatically loads schema files with extensions .graphqls or
.gqls in the directory src/main/resources/graphql. All you need to do is save the
22
Schema
schema file in the correct location. You can alternatively load schema files from a different
location, see details in the documentation1 .
Under the hood, Spring for GraphQL automatically reads the schema files, parses the
files, and instantiates GraphQL Java’s GraphQLSchema object with the schema.
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.
Another classification of types differentiates between input and output types. An output
type is a type that describes the result of a request, while input types are used to describe
input data for a GraphQL request. We will cover requests in greater detail in the next
chapter on GraphQL query language.
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.
In GraphQL Java, a field is represented by an instance of GraphQLFieldDefinition for
object and interface fields, or GraphQLInputObjectField for input objects.
1
https://docs.spring.io/spring-graphql/docs/current/ref erence/html/#execution-graphqlsource-
schema-resources
23
Schema
24
Schema
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.
In SDL, custom scalars need to be declared explicitly, while built-in scalars can be used
without any declaration.
"""
A custom scalar representing a date without any time zone.
Note how this custom scalar must be declared explicitly.
"""
scalar Date @specifiedBy(url:
"https://scalars.graphql.org/andimarek/local-date")
type Pet {
"String is a built-in scalar, therefore no declaration is required."
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.
Here is how to declare an enum in SDL:
enum PetKind {
CAT, DOG, BIRD
}
type Pet {
25
Schema
name: String
kind: PetKind # used as output type
}
type Query {
pets(kind: PetKind!): [Pet] # used as input type
}
Alternatively, enums can also be declared with each value on its own line. This is because
a comma , is considered whitespace and is ignored.
enum PetKind {
CAT
DOG
BIRD
}
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.
An object type is declared in SDL as type.
type Pet {
name: String
color: String
friend: Pet # recursive reference
owners: [Person!] # A list of people
}
type Person {
name: String
}
type Query {
pet(name: String!): Pet # lookup a pet via name
}
26
Schema
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
27
Schema
interface Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
}
# Another implementation
type Cat implements Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
doesMeow: Boolean # additional field specific to Cat
}
It might surprise you that all interface fields must be repeated. As we can see in this
example, Cat and Dog both repeat the same name field from Pet. This was a deliberate
decision by the GraphQL working group to focus more on readability than shorter
notation.
In GraphQL Java, an interface is represented as an instance of GraphQLInterfaceType,
and fields by instances of GraphQLFieldDefinition.
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.
In SDL, a union is declared as a list of object types separated by a vertical bar |.
type Dog {
name: String
doesBark: Boolean
}
type Cat {
28
Schema
name: String
doesMeow: Boolean
}
Wrapping types contain another type, which can be another wrapping type or named
type. Ultimately, a wrapping type wraps a named type.
A list type is a list of the wrapped type. A non-null type marks this type as never being
null.
A wrapping type can be used as a type for a field argument, as a field or input field
type.
In SDL notation, a non-null type is declared by appending an exclamation mark ! to
the type. A list is declared by surrounding the type with brackets [ ].
type Query {
pet(id: ID!): Pet
}
type Pet {
id: ID!
ownerNames: [String!] # A combination: a list of non-null strings
}
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
29
Schema
# 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 default value can be optionally defined with the equals sign =. In the following example,
you can fetch a specific number of pets. Alternatively, if the number of pets is not
specified, it will default to 20.
type Query {
pets(howMany: Int = 20): [Pet]
}
30
Schema
type Query {
"all currently known pets"
pets: [Pet]
}
"""
A Pet can be a Dog or or Cat.
A Pet has a human owner.
"""
type Pet {
name: String
owner: Person
}
These descriptions are collated into documentation, for example in the GraphiQL play-
ground3 in Figure 2.
To access documentation in GraphiQL, click on the book icon in the top left corner.
See the Introduction chapter for how to add GraphiQL to your Spring for GraphQL
application.
The GraphQL specification4 recommends that the schema and all other definitions
(including types, fields, and arguments) should provide a description unless they are
considered self-descriptive.
Comments
Comments start with a hash sign # and everything on the same line is considered a part
of a comment. For example:
2
https://commonmark.org/
3
https://github.com/graphql/graphiql
4
https://spec.graphql.org/draft/#sec-Descriptions
31
Schema
type Query {
# This is a comment
hello: String
}
Note that comments are very different to descriptions. Descriptions are used to construct
documentation for the schema, which is made available via introspection. Comments are
ignored like whitespace, and are not used in documentation.
In this chapter, we discussed the schema elements that describe the structure of an API.
In the DataFetchers chapter, we’ll discuss how this API description connects to the logic
to execute requests. Before we get to DataFetchers, let’s discuss the GraphQL query
language.
32
GraphQL query language
The GraphQL query language is the domain-specific language (DSL) that enables con-
sumers define what they would like to do.
Note that the phrase “query language” encapsulates queries, mutations, and subscriptions,
as we’ll discuss in this chapter.
The query language syntax is deeply related to the GraphQL schema. A GraphQL
schema defines which queries, mutations, or subscriptions are valid in query language. In
the query language examples below, assume there is a corresponding GraphQL schema
that makes the query language valid.
Literals
The query language contains several literals that mirror the schema input types.
Operations
33
GraphQL query language
Query operations
A query operation requests data and returns a response containing the result.
A simple query called myQuery, fetching a single field someField, is written like this:
query myQuery {
someField
}
A query operation is a tree of selected fields. The fields on the first level of the query are
called root fields. The fields below another field are a sub-selection. Every selected field
in query operation must match their respective schema definitions.
In this example operation, pet is a root field, and name is a sub-selection.
query petName {
pet {
name
}
}
Selected fields in this petName query correspond to their respective schema definitions:
type Query {
pet: Pet
}
type Pet {
name: String
}
A key feature of GraphQL is that only selected fields are returned, no more and no less.
To enable this feature, fields of object, interface, and union types require a sub-selection.
Every field must be explicitly selected, there are no wildcard selections. Requiring sub-
selections was a deliberate decision in the GraphQL spec to ensure queries are predictable
and therefore clients always receive exactly what they ask for.
As a more complex example, a query can request the country of the address of the owner
of a pet.
34
GraphQL query language
query petOwnerDetails {
pet {
name
owner {
name
address {
country
}
}
}
}
Queries are always validated against the schema of the API. We cannot query fields that
are not defined in the schema.
For this schema:
type Query {
pet: Pet
}
type Pet {
name: String
}
We will not be able to execute the following invalid query. An error will be raised because
there is no nickName field on the Pet type.
query invalid {
pet {
name
nickName
}
}
Although there is a difference between a “field definition” in schema and a “field” used in
query language, we often use the word field for both if the context is clear.
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.
35
GraphQL query language
Declare a mutation operation with the keyword mutation followed by the name of the
mutation:
mutation myMutation {
changeSomething
}
Every root field of a mutation operation must be a field of the Mutation object type in
the schema. Only the root fields of a mutation can have side effects, as required by the
GraphQL spec.
Sub-selections of the root field of a mutation operation are semantically equal to queries.
In practice, it’s useful for a mutation to return some of the changed data. For example,
after updating the name of a User, it’s useful to receive the newly changed User in the
response.
For example, in the schema, the changeUser field is defined in the Mutation type:
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
}
}
}
36
GraphQL query language
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.
For example, if the client wants to be informed about every new email matching certain
criteria:
subscription newMessage {
newEmail(criteria: {
sender: "luna@example.com",
contains: "playing" }
) {
text
}
}
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.
The execution of a subscription and the handling of the request on the transport layer
differs significantly from queries and mutations. This is simply because a long-lived
subscription request reacting to certain events is more complicated than a simple process
of query (or mutation) request, execution, and response. We’ll discuss this further in the
Subscriptions chapter.
37
GraphQL query language
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
=).
For example, a schema defining a pet field with a required id argument:
type Query {
pet(id: ID!): Pet
}
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
}
}
Arguments can be any literal, including null, as long as it conforms to the schema. For
example, here’s a query with an input object literal.
query petAgeSearch {
pets(filter: { minAge: 10, maxAge: 20 }) {
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.
38
GraphQL query language
Fragments are declared outside of operations. The fragment definition syntax is:
... <FragmentName>
For example, let’s define a personDetails fragment, to reuse common repeated fields of
Person. Let’s use the fragment in the petOwners query.
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.
Inline fragments are different to fragments, as they are declared inline rather than outside
an operation. Unlike fragments, inline fragments have no name, and cannot be reused.
The inline fragment syntax is:
39
GraphQL query language
... on <TypeCondition> {
<fields>
}
type Query {
pets: [Pet]
}
interface Pet {
name: String
}
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
}
}
}
Depending on the Pet type, we select different fields. We are only interested in doesBark
for Dogs, while for Cats we are only interested in doesMeow.
Types implementing interfaces will likely add additional fields which are not shared across
all implementations. For example, the doesBark field only appears in the Dog type.
40
GraphQL query language
Fragments or inline fragments must be used to query fields which are not guaranteed
across all implementations.
As a counterexample, the query below is invalid because there is no doesBark field in
the Pet interface.
query invalid {
pets {
doesBark
}
}
Fragments or inline fragments must be used for union types. A GraphQL union represents
an object that could be one of a list of types, but a union does not define any fields
itself.
For example, consider this example schema with two important food groups:
type Query {
dinner: [Food]
}
type Pizza {
name: String
toppings: [String]
}
type IceCream {
name: String
flavors: [String]
}
A query for dinner must include fragments or inline fragments, because the union does
not define any fields itself.
query healthyDinner {
dinner {
... on Pizza {
name
toppings
}
... on IceCream {
41
GraphQL query language
name
flavors
}
}
}
The following query is invalid, because the union type Food does not define any fields.
query invalid {
dinner {
toppings
flavors
}
}
Variables
42
GraphQL query language
It’s possible to have multiple variables. In this example mutation, the first variable is
required and the second is not required.
Variable values are sent alongside the operation. For example, we want to find a pet with
ID 9000.
If providing variable values as JSON, to find a pet with ID 9000, we would send:
{
"petId": "9000"
}
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.
Define an alias with the name, followed by a colon:
query aliases {
alias1: someField
alias2: someField
}
Although simple renames are handy, aliases are more often used to query the same field
multiple times with different arguments. As the response key is set to the field name by
default, aliases are essential if we want to use the same field twice. For example:
43
GraphQL query language
type Query {
search(filter: String!): String
}
query searches {
search1: search(filter: "Foo")
search2: search(filter: "Bar")
}
{
"search1": "Foo result",
"search2": "Bar result"
}
GraphQL document
The operation name is the word following the query, mutation, or subscription keyword.
In the following example, the operation name is findPet.
In the GraphQL document, if there are multiple operations, they must all have a name.
If there is only one operation, it can be unnamed.
Although the specification permits unnamed operations, we strongly suggest using
operation names. They give a meaningful name that explains the intention of an
operation. Operation names are invaluable for observability in a production GraphQL
service. For example, operation naming helps track query usage over time. You could
44
GraphQL query language
also ask consumers to use operation names that identify the caller, so it’s easier to track
who is using the query.
Nevertheless, we will briefly cover unnamed operations, as they do appear in documenta-
tion examples and elsewhere.
If there is only one operation, it can be unnamed. For example this unnamed mutation:
mutation {
changeName(name: "Foo")
}
Where the single unnamed operation is a query, the keyword query can be dropped. The
two following examples are equivalent:
query {
hello
}
{
hello
}
Where there are multiple operations in a document, they must all be named. Here’s an
example document with a fragment:
query hello {
...helloFragment
}
mutation changeName {
changeName(name: "Foo")
}
subscription nameChanged {
nameChanged
}
45
GraphQL query language
46
DataFetchers
A DataFetcher loads data for exactly one field. It’s the most important concept for
executing a GraphQL request, because it’s the logic that connects your schema and your
data.
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.
A quick note: DataFetchers are called Resolvers in the GraphQL specification and in
other implementations. I (Andi) named it DataFetcher because I thought it reflected the
purpose better. I am not convinced I would make the same decision today, but now it is
too late to change.
A GraphQL operation is basically a tree of fields. The execution is field-oriented: for
each field, GraphQL Java loads the data. However, given that GraphQL is agnostic
about where the data comes from, we must tell GraphQL Java how to load the data.
GraphQL Java needs to know how to load the data for every field, therefore every
field has an associated DataFetcher.
This field-oriented data loading approach differs from REST, where we implement the
logic per endpoint for resources. In GraphQL, there is only one endpoint and data loading
happens per field depending on the request. This carries over to Spring controllers, where
we map every field DataFetcher to a method in a Controller. This means that Spring for
GraphQL controller methods represent a GraphQL field, instead of a REST resource.
Although every field has an associated DataFetcher, in practice you don’t have to manually
write a DataFetcher for every field. In the PropertyDataFetcher section later in this
chapter, we’ll show how most fields can be automatically mapped to DataFetchers.
47
DataFetchers
type Query {
favoritePet: Pet
}
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
48
DataFetchers
lastName: String
}
package myservice.service;
package myservice.service;
This is how to register the two DataFetchers with Spring for GraphQL’s controller
annotations.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
record PetsController(PetService petService) {
@QueryMapping
Pet favoritePet() {
return petService.getFavoritePet();
}
49
DataFetchers
The @QueryMapping annotation is a shortcut. You can alternatively specify a field with
the @QueryMapping annotation, or use the general @SchemaMapping annotation.
The owner method is annotated with the general @SchemaMapping annotation. The
annotation’s attributes indicate this method will be registered as a DataFetcher for the
owner field in the Pet type.
In addition to registering DataFetchers automatically, these annotated methods allow for
convenient access to different inputs. In the example above, the owner DataFetcher takes
a parameter Pet pet, which contains the source or parent object Pet, which is necessary
to determine the owner’s name.
In the @SchemaMapping annotation in our example, we can take an extra shortcut and
remove the typeName and field attributes. When these attributes are not provided,
Spring for GraphQL will register the typeName as the simple class name of the source or
parent object injected into the method (Pet), and the field name will default to the
name of the method (owner).
You might be wondering why only two DataFetchers are registered in this controller,
considering there were 5 fields in the schema. We’ll see how the remaining fields were
automatically wired with DataFetchers in the PropertyDataFetcher section up next in
this chapter.
We stated that every field has an associated DataFetcher. However, in the Pet
example earlier, we had five fields in the schema and only implemented two DataFetchers
with controller annotations. This was not a mistake, and we have not forgotten anything!
This is actually a realistic scenario.
While every field has a DataFetcher, we only need to implement a few DataFetchers
ourselves. The rest are default DataFetchers that GraphQL Java automatically generates,
which are called PropertyDataFetchers. This is illustrated in Figure 1.
A PropertyDataFetcher is the perfect choice when the schema matches the
Java object returned by the parent DataFetcher.
Returning to our Pet example is the best way to understand this: we have a
PropertyDataFetcher for 3 different fields: Pet.name, Person.firstName, and
Person.lastName.
The Pet record class returned by the PetService is:
50
DataFetchers
package myservice.service;
package myservice.service;
51
DataFetchers
GraphQL field matches the Java object property. In our example, it means the
Java property follows the Java getter naming convention.
The PropertyDataFetcher can also fetch data from Java fields, is compatible with record
classes, and can also access values from a java.util.Map.
The Map support is particularly useful to quickly mock DataFetchers. For example, our
owner DataFetcher can return the same Person using a Map.
@SchemaMapping
Map<String, String> owner(Pet pet) {
return Map.of("firstName","Andi",
"lastName","Marek");
}
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.
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. For an introduction to interfaces
and unions, see the earlier Schema chapter.
For example, let’s consider a Pet interface, which is implemented by Cat and Dog types.
52
DataFetchers
type Query {
favoritePet: Pet
}
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.
The default ClassNameTypeResolver is sufficient if your Java model maps 1:1 with your
API, without any further configuration. Optionally, you can modify the behaviour of the
default ClassNameTypeResolver (see documentation1 ) or manually provide your own
TypeResolver via the RuntimeWiringConfigurer.
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.
1
https://docs.spring.io/spring-graphql/docs/current/reference/html/#execution.graphqlsource.defau
lt-type-resolver
53
DataFetchers
package myservice.service;
interface Pet {
String name();
String ownerId();
}
package myservice.service;
package myservice.service;
Then add two owner DataFetchers for Dog and Cat types.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@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());
54
DataFetchers
query bestPet {
favoritePet {
name
owner {
firstName
}
...on Dog {
doesBark
}
...on Cat {
doesMeow
}
}
}
If you have encountered a case where the default ClassNameTypeResolver is not suitable,
you can manually register a TypeResolver by creating a RuntimeWiringConfigurer
bean. The RuntimeWiringConfigurer has RuntimeWiring.Builder as a parameter.
package myservice.service;
import graphql.schema.TypeResolver;
import graphql.schema.idl.TypeRuntimeWiring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@Configuration
class Config {
TypeResolver petTypeResolver = (env) -> {
// Your custom type resolver logic
};
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
55
DataFetchers
wiringBuilder.type(petWiring);
};
}
When we have a closer look at TypeResolvers in GraphQL Java later in this chapter,
we’ll see how to manually write our own TypeResolver with pure GraphQL Java.
type Query {
search(pattern: String, limit: Int): String
}
With Spring for GraphQL, the arguments are declared with the @Argument annotation.
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class SearchController {
@QueryMapping
String search(@Argument String pattern, @Argument int limit) {
// Your search logic here
}
56
DataFetchers
The @Argument annotation automatically takes the method parameter name, if available,
as the name for the GraphQL argument. Note this requires the -parameters compiler
flag with Java 8+ or debugging information from the compiler. You most likely have this
flag already enabled.
It’s also possible to customise the name through the annotation, as in this example:
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class SearchController {
@QueryMapping
String search(@Argument("pattern") String searchPattern,
@Argument("limit") int maxElements) {
// Your search logic here
}
Spring for GraphQL makes it much easier to use input object arguments. We’ll see later
in this chapter that in GraphQL Java, we always get java.util.Map for input objects.
Spring for GraphQL makes this step easier by binding GraphQL arguments to Java
classes automatically, if they are compatible.
For example, a search schema with an input object argument:
type Query {
search(input: SearchInput): String
}
input SearchInput {
pattern: String
limit: Int
}
57
DataFetchers
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@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.
Without Spring for GraphQL’s argument injection, the input object would be a map,
which is not as convenient to work with.
package myservice.service;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.Map;
@Controller
class SearchController {
@QueryMapping
String search(DataFetchingEnvironment env) {
58
DataFetchers
Spring for GraphQL also supports the following method parameters for schema mapping
handler methods.
Argument Description
@Arguments Binding all arguments to a single object
“Source” Access to the source (parent) instance of 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
GraphQLContext The entire GraphQLContext
java.security.Principal The currently authenticated principal that made this
request. See the chapter about Security for more. This
is SecurityContext.getAuthentication()
DataFetchingFieldSelectionSet The DataFetchingFieldSelectionSet from
DataFetchingEnvironment
Locale or Optional<Locale> The current locale for the request, from
DataFetchingEnvironment
DataFetchingEnvironment The entire DataFetchingEnvironment
In the Schema chapter, we discussed that in addition to the built-in scalar types, it is
possible to add custom scalars.
A common custom scalar to add is Date. In the Schema chapter we saw that custom
scalars must be declared in the schema:
59
DataFetchers
"""
A custom scalar representing a date without any time zone.
Note how this custom scalar must be declared explicitly.
"""
scalar Date @specifiedBy(url:
"https://scalars.graphql.org/andimarek/local-date")
type Pet {
"String is a built-in scalar, therefore no declaration is required."
name: String
dateOfBirth: Date
}
You can use custom scalar implementations from libraries or implement your own. In
this example we’ll demonstrate the GraphQL Java Extended Scalars2 library, which is
maintained by the GraphQL Java team.
To use the Date scalar in the GraphQL Java Extended Scalars library, add the package.
For Gradle, add this to your build.gradle file:
implementation 'com.graphql-java:graphql-java-extended-scalars:19.1'
For Maven:
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
<version>19.1</version>
</dependency>
Note: the major version number of the Extended Scalars library corresponds to the linked
major version of the main GraphQL Java release. As examples in this book were written
with Spring for GraphQL 1.1.2 which uses GraphQL Java 19.2, we’ll use Extended Scalars
19.1.
To wire custom scalars in Spring for GraphQL, create a RuntimeWiringConfigurer
bean. This will link the Date implementation in graphql-java-extended-scalars to
the scalar Date declared in your schema.
2
https://github.com/graphql-java/graphql-java-extended-scalars
60
DataFetchers
package myservice.service;
import graphql.scalars.ExtendedScalars;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@Configuration
class GraphQlConfig {
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder ->
wiringBuilder.scalar(ExtendedScalars.Date);
}
Spring for GraphQL automates much of the work to register DataFetchers with schema
fields. By using Spring for GraphQL controller annotations, you might never even see
the words DataFetcher nor RuntimeWiring.
To build a deeper understanding of how Spring for GraphQL connects data fetching
logic to the schema, we are going to discuss how GraphQL Java works under the hood.
In your Spring for GraphQL application, you won’t need to write any of the code in
the remainder of this chapter, but we hope that by showing you the fundamentals, we
concretely explain the “magic” of Spring.
In this second half of the chapter, we will build up to an executable schema with pure
GraphQL Java. A complete end-to-end example is presented later in this chapter.
61
DataFetchers
Earlier in this chapter we covered how to create and register DataFetchers with Spring
for GraphQL’s controller annotations such as @QueryMapping and @SchemaMapping.
Now let’s see how to write the equivalent code for the Pet schema without any Spring
automated “magic”.
A DataFetcher loads data for exactly one field. Inside GraphQL Java, it is represented
as a very generic Java interface.
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
}
As a general rule, at least every root field needs to have a DataFetcher implemented.
Let’s implement the DataFetcher for Query.favoritePet. Note that the logic is identical
to the favoritePet controller method in the Spring for GraphQL example earlier in this
chapter.
62
DataFetchers
This DataFetcher also reflects best practice: a DataFetcher should be slim. A DataFetcher
should only take care of GraphQL-specific aspects, and delegate the actual data retrieval
to a layer below.
This particular favoritePetDataFetcher is quite simple, and does not make use of the
DataFetchingEnvironment (env) at all.
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;
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.
63
DataFetchers
@SchemaMapping
Person owner(Pet pet) {
return petService.getPerson(pet.ownerId());
}
Spring for GraphQL injects the Pet source object as a method parameter, rather than hav-
ing to manually access it from the DataFetchingEnvironment with env.getSource().
Let’s expand on the previous example to understand better how DataFetchers work in
practice and discuss the important source object.
The source comes from the result of the parent field DataFetcher, which was
executed before the child DataFetcher. The source can be anything, so the actual
method signature in DataFetchingEnvironment is very generic.
We know that the parent DataFetcher favoritePet returned a Pet. We can access the
parent DataFetcher result via the source object of the DataFetchingEnvironment.
return petService.getPerson(pet.ownerId());
We can always safely assume that source is not null, except for the root fields because
a root field doesn’t have a parent field. If a DataFetcher returns null, which can be a
valid response, the execution stops and the child DataFetchers are never invoked.
64
DataFetchers
The source object is specific for every non-root DataFetcher. It gives each child
DataFetcher a source of data and additional information.
The source object also couples the DataFetchers together. The ownerDataFetcher
is not generic logic that can load any Person, rather it requires that the parent field
DataFetcher has loaded a Pet object.
Spring for GraphQL’s schema mapping annotations implement DataFetchers and auto-
matically register them to their correct field and type coordinates in the schema in the
GraphQLCodeRegistry inside RuntimeWiring.
At the start of the annotated methods section of this chapter, we mentioned there are
a few key GraphQL Java classes which work closely with DataFetchers. Let’s see how
these classes come together to create an executable schema in GraphQL Java.
A GraphQLSchema is both the structure of the API, and all the logic needed to execute
requests against it.
65
DataFetchers
We saw earlier in this chapter that another key concept RuntimeWiring, contains
GraphQLCodeRegistry, which maps schema fields to DataFetchers. Each DataFetcher
needs to be registered in the GraphQLCodeRegistry inside RuntimeWiring.
In pure GraphQL Java code, here is a creation of GraphQLSchema from start to finish,
and an example of a query being executed.
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.Pet;
import myservice.service.PetService;
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse(sdl);
66
DataFetchers
The schema is first parsed from a string. This schema could alternatively be parsed
from a file. Then, the TypeRuntimeWiring objects containing our two DataFetchers are
instantiated. Then the TypeRuntimeWiring objects are registered in the RuntimeWiring.
Finally, the executable schema is created by combining both the parsed schema and
the RuntimeWiring. Luckily, you won’t have to write any of this boilerplate code
to instantiate an executable schema, as this is automatically managed by Spring for
GraphQL.
67
DataFetchers
If the type of the field is an interface or union, GraphQL Java needs to determine the
actual object of the value via a TypeResolver. In GraphQL Java, a TypeResolver is a
very generic Java interface:
Every interface and union type has an associated TypeResolver, similar to how every field
has an associated DataFetcher. The TypeResolver interface needs to be implemented.
If using GraphQL Java without Spring for GraphQL’s default ClassNameTypeResolver,
we would have to implement our own TypeResolver. It’s worth reviewing this example
to understand how a TypeResolver works without the Spring magic. For example, let’s
write a Pet TypeResolver for this expanded Pet schema.
type Query {
favoritePet: Pet
}
interface Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
68
DataFetchers
If using GraphQL Java directly, this is how to implement a TypeResolver for the Pet
interface above. In this example, we map Java classes to schema types. However, you
could implement any logic here to resolve types.
69
DataFetchers
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.
In this chapter, we covered the essentials of DataFetchers, the logic that connects your
schema to your data. We saw how to create DataFetchers in Spring for GraphQL. We
then took a look under the hood in GraphQL Java. We saw how DataFetchers work in
GraphQL Java, and re-created the same DataFetchers and executable schema without
the Spring “magic”.
We hope you have a much better understanding of DataFetchers and how Spring for
GraphQL eliminates much of the boilerplate code. We will cover more advanced topics in
the DataFetchers in depth chapter and take a deep dive into execution in the Execution
chapter.
70
Building a GraphQL service
This chapter focuses on the more hands-on aspects of building a service with Spring for
GraphQL, and makes use of the concepts we covered in the previous chapters. We will
build upon the service we started in the Introduction chapter. By the end of this chapter,
you will implement a more substantial GraphQL service.
1
https://github.com/spring-projects/spring-boot
71
Building a GraphQL service
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.
The GraphQL Java homepage is https://www.graphql-java.com and the source code can
be accessed on GitHub at https://github.com/graphql-java/graphql-java.
GraphQL Java has minimal dependencies to maximize its usage. SLF4J2 is the only key
dependency.
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.
Spring for GraphQL supports both reactive Spring WebFlux and Spring MVC. When
starting a new project, choose either
org.springframework.boot:spring-boot-starter-webflux
or
org.springframework.boot:spring-boot-starter-web
Reading schemas
2
https://www.slf4j.org/
72
Building a GraphQL service
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.
GraphiQL: GraphiQL is an interactive, in-browser GraphQL IDE. Spring for GraphQL
comes with built-in GraphiQL support, but it’s not enabled by default. Activate it with
spring.graphql.graphiql.enabled=true. The default GraphiQL path is /graphiql,
which can be modified with spring.graphql.graphiql.path.
Schema files: Spring for GraphQL scans Schema Definition Language (SDL)
files with default file extensions *.graphqls and *.gqls from the default location
classpath:graphql/**/.
You can modify file extensions with spring.graphql.schema.locations.file-extensions
and modify the location with spring.graphql.schema.locations.
In the “Your first Spring for GraphQL service” section of the introduction chapter, we
built a minimal Spring for GraphQL service and executed our first query. Let’s revisit
key Spring for GraphQL concepts from the past few chapters and build a more fully
featured GraphQL service.
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.
We have a simple query field pets which returns basic information about Pets. You should
already have this schema file saved in the file src/main/resources/graphql/schema.graphqls.
type Query {
pets: [Pet]
}
type Pet {
name: String
73
Building a GraphQL service
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.
We will use Spring for GraphQL’s annotation-based programming model to register
DataFetchers, which was previously discussed in the DataFetchers chapter. @Controller
components use annotations to declare handler methods as DataFetchers for specific
GraphQL fields. As we have a query field pets, let’s use the shortcut @QueryMapping
annotation. We’ll represent Pets in Java as a record class.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class PetsController {
@QueryMapping
List<Pet> pets() {
return List.of(
new Pet("Luna", "cappuccino"),
new Pet("Skipper", "black"));
}
74
Building a GraphQL service
and shortcut versions such as @QueryMapping, see the annotated methods section in the
DataFetchers chapter.
In this example, we don’t need to manually write DataFetchers for the Pet fields name
and color. Recall that PropertyDataFetchers for name and color are automatically
registered, because the GraphQL fields match the Java object’s properties. For more on
PropertyDataFetchers, see the earlier DataFetchers chapter.
The pets() method is currently returning an in-memory list. We’ll change this to call
another service in the next section.
GraphQL is agnostic about the source of your data. Let’s upgrade our data source from
an in-memory list to a more realistic scenario. In a production service, it’s likely you
want to fetch data from another service, such as a REST service with the help of Spring
WebClient.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
@Controller
class PetsController {
WebClient petWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service").build();
}
@QueryMapping
Flux<Pet> pets() {
return petWebClient.get()
.uri("/pets")
.retrieve()
.bodyToFlux(Pet.class);
}
75
Building a GraphQL service
In our upgraded pets DataFetcher, the bodyToFlux call returns a Flux<Pet> based on
the JSON response of http://pets-service/pets. This is a placeholder URL in place
of a REST endpoint. If you do not yet have an external service in mind, you can revert
to using in-memory objects for the following examples.
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
}
query petsAndOwners {
pets {
name
owner {
name
}
}
}
Our external Pet service only contains a reference to an owner with ownerId. To retrieve
information such as the owner’s name, we have to contact a separate owner service. To
model this, add the field ownerId to the Pet class.
76
Building a GraphQL service
package myservice.service;
package myservice.service;
To fetch the owner information for a given list of pets, we need to implement a new
DataFetcher called owner that takes a Pet as an argument. The argument Pet is called
the “source object” since it determines the pet we retrieve owner data for.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service").build();
this.ownerWebClient = builder.baseUrl("http://owner-service")
.build();
}
@QueryMapping
Flux<Pet> pets() {
return petWebClient.get()
.uri("/pets")
.retrieve()
.bodyToFlux(Pet.class);
77
Building a GraphQL service
// 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.
Every time we fetch data for a non-root field (such as owner), we use a source object as an
argument to identify the parent for returned data from DataFetcher. See the DataFetcher
chapter for more on the source object in Spring for GraphQL and how it is represented
in GraphQL Java.
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 {
78
Building a GraphQL service
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
}
}
We can conveniently access this GraphQL argument by using Spring for GraphQL’s
@Argument annotation. See more on arguments in Spring for GraphQL in the DataFetchers
chapter.
Let’s add the id field to the Pet class.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Controller
class PetsController {
WebClient petWebClient;
79
Building a GraphQL service
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service").build();
this.ownerWebClient = builder.baseUrl("http://owner-service")
.build();
}
@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);
}
As we saw in the Query Language chapter, an argument can also be an input object.
An input object type describes a group of input fields where each has an input type. In
SDL, an input object is declared with the input keyword.
Let’s introduce a new query field petSearch which takes an input object
PetSearchInput.
80
Building a GraphQL service
type Query {
pets: [Pet]
pet(id: ID!): Pet
petSearch(input: PetSearchInput!): [Pet] # New field
}
type Pet {
id: ID!
name: String
color: String
owner: Person
}
type Person {
name: String
}
Let’s add a Java class PetSearchInput to represent the input type in the schema. To
access this input object as an argument, we add it as a parameter to the petSearch
method.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Controller
81
Building a GraphQL service
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service").build();
this.ownerWebClient = builder.baseUrl("http://owner-service")
.build();
}
@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);
}
// New
@QueryMapping
Flux<Pet> petSearch(@Argument PetSearchInput input) {
// perform the search
}
82
Building a GraphQL service
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”.
83
Building a GraphQL service
mutation changeName {
changePetName(id: "123", newName: "Mixie") {
pet {
name
}
}
}
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation
.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service").build();
this.ownerWebClient = builder.baseUrl("http://owner-service")
.build();
84
Building a GraphQL service
@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)
85
Building a GraphQL service
.body(BodyInserters.fromValue(changeNameBody))
.retrieve()
.bodyToMono(ChangePetNamePayload.class);
}
In our changePetName method, we update the name of a pet with an HTTP PUT request.
The response from the HTTP PUT contains the newly modified pet’s details.
A mutation controller method can have any of the method arguments available to schema
mapping handler methods. See the DataFetchers chapter for a list of method arguments.
In this case, the changePetName method takes the two parameters id and newName,
representing arguments of the mutation field.
To demonstrate unions and interfaces, we will use a simpler example to demonstrate how
Spring for GraphQL’s default TypeResolver works. If you would like to use this Pet
interface in combination with the earlier examples of this chapter, we’ll leave the task of
deserializing data up to you, as deserialization depends on the data source you choose.
There are many kinds of pets in the world, each with slightly different attributes. It
would be better to represent Pet as an interface in our schema. Let’s add two Pet
implementations, Dog and Cat. You can add your favourite Pet implementation too.
To demonstrate unions, we’ll also add a Creature union, and a new query field for
creatures.
type Query {
creatures: [Creature] # New
}
86
Building a GraphQL service
name: String
color: String
barks: Boolean
}
# 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;
87
Building a GraphQL service
package myservice.service;
package myservice.service;
Let’s register a DataFetcher for the new creatures query field. Note that the incoming
data must be converted into the correct Java class representation. This will depend on
the implementation of the remote service. For example, the service may return a field for
each Creature to indicate its type.
In the example below we have included an in-memory list, so you can verify this union
works before setting up how to request from the remote service and how to deserialize
data.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
@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"));
88
Building a GraphQL service
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
}
}
}
If a field type is an interface or union, GraphQL Java needs to determine the actual
object type of the value via a TypeResolver.
In this service, we will make use of Spring for GraphQL’s default ClassNameTypeResolver,
which 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. The default ClassNameTypeResolver is sufficient because in our
service, the Java model maps 1:1 with the API. If your model doesn’t exactly match
your API, see the TypeResolver section in the DataFetchers chapter for how to register a
custom TypeResolver.
The default ClassNameTypeResolver is registered when the Spring Boot starter initializes
graphql.GraphQL, and therefore no further configuration is required for our service. We
do not need to manually write a TypeResolver for either the Pet interface nor the
Creature union.
In this chapter, you built a more substantial Spring for GraphQL application making use
of concepts we have discussed in previous chapters.
89
Building a GraphQL service
If you haven’t already, enable the GraphiQL interactive playground by adding this to
your application.properties file.
spring.graphql.graphiql.enabled=true
90
Subscriptions
There are three types of operations supported by GraphQL: queries, mutations, and
subscriptions. In this chapter we’ll expand on the subscription operation. While queries
and mutations are similar in many respects, subscriptions are quite different.
A subscription is a long-lived request, where clients want to be informed about certain
events on the server side. A subscription can be active for minutes or hours, whereas
a query or mutation is active for a few milliseconds to a few seconds at most. With
a subscription, the clients will get updates from the server for every relevant change,
meaning that there is a “stream” of data, rather than a single response.
Let’s walk through an example of an online store. The client wants to be notified each
time someone creates a new order, when it happened, and by whom. The subscription
operation would look like this:
subscription myOrders {
newOrderCreated {
id
createdTime
customer {
name
}
}
}
First, the client requests a newOrderCreated subscription and then receives the first
response 10 seconds later.
{
"newOrderCreated": {
"id": "123",
"createdTime": "2015-07-06T04:11:11.000Z",
"customer": {
"name": "Andi"
}
}
}
91
Subscriptions
{
"newOrderCreated": {
"id": "124",
"createdTime": "2015-07-06T04:11:13.000Z",
"customer": {
"name": "Elli"
}
}
}
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.
Protocol-wise, subscriptions also need a different solution, because HTTP was not
designed for data to be sent from the server to the client via a long-lived connection.
Most commonly, WebSockets are used as the protocol for subscriptions. Spring for
GraphQL supports the WebSocket protocol out of the box.
Getting started
type Subscription {
hello: String
}
92
Subscriptions
In the controller, add an annotated method hello. This method is annotated with
@SubscriptionMapping, which registers this DataFetcher to the subscription field hello
in the schema.
package myservice.service;
import org.springframework.graphql.data.method.annotation
.SubscriptionMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
@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);
}
As the results of subscriptions are streams of data, Flux from Reactor is a perfect
abstraction. In our example, we emit data three times: “Hello 1”, “Hello 2” and “Hello
3”, each with a one-second delay.
In order to enable subscriptions, we have to add a new Spring configuration property,
setting the path where we want to expose our subscription.
spring.graphql.websocket.path=/graphql
spring.graphql.graphiql.enabled=true
Let’s test our subscription. Start the service and open the GraphiQL playground at
http://localhost:8080/graphiql.
In the playground, execute a new subscription. Click the play button to execute, and
you should see a response like Figure 1.
93
Subscriptions
subscription myFirstSubscription {
hello
}
Then we will see the response changing every second, from “Hello 0” to “Hello 1” to
“Hello 2”, as shown in Figure 2.
{
"errors": [
{
"isTrusted": true
94
Subscriptions
}
]
}
Please double-check that you have enabled the WebSocket path in your configuration
file.
spring.graphql.websocket.path=/graphql
Execution
# Not valid
subscription tooManyRoots {
newCats {
name
}
newDogs {
name
}
}
1
https://www.reactive-streams.org/
95
Subscriptions
subscription myOrders {
newOrderCreated {
id
createdTime
customer {
name
}
}
}
The DataFetcher for newOrderCreated returns a Publisher. Every new event emitted
from it starts a normal query execution for the sub-selection with the event payload as
source object.
Let’s walk through subscription execution step by step:
We receive a new subscription request from a client, which is executed by calling the
DataFetcher for newOrderCreated, that returns a Publisher.
When that Publisher emits a new event, we execute the sub-selection { id createdTime
customer } with the event payload as a source object. All of this happens exactly the
same way as described in the fetching data section of the Execution chapter: we invoke
all three DataFetchers for id, createdTime, and customer and complete the returned
values. If customer returns a non-null value, we invoke the DataFetcher for name.
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-ws2 . Spring for GraphQL supports this protocol out of the box.
This protocol enables any kind of GraphQL requests to be executed with it, but in
practice it is used primarily for subscription requests because the WebSocket protocol is
more complicated than HTTP.
2
https://github.com/enisdenjo/graphql-ws
96
Subscriptions
To activate the WebSocket path, set the path via the spring.graphql.websocket.path
configuration property.
spring.graphql.websocket.path=/graphql-subscriptions
The GraphiQL playground separates HTTP and WebSocket URLs, so we can set two
different endpoints when we open GraphiQL.
http://localhost:8080/graphiql?path=/graphql&wsPath=/graphql-subscriptions
Spring for GraphQL handles the URLs for us automatically. When we open
http://localhost:8080/graphiql, we get redirected automatically to a URL with the
correct parameters depending on our configuration.
It is also possible to offer both the WebSocket protocol and normal HTTP protocol via
the same URL, as we did in our example earlier in the chapter.
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 repo3 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.
3
https://github.com/enisdenjo/graphql-ws#recipes
97
Request and response
In this chapter, we will take a closer look at requests and responses for GraphQL, including
the HTTP protocol.
It might surprise you that the GraphQL spec does not specify any transport protocol for
how a client communicates with a server. Although it was a deliberate decision to exclude
transport concerns from the spec, in practice HTTP has become the most commonly used
protocol. The community agrees on the HTTP spec outlined in Serving over HTTP best
practices1 on http://graphql.org, which serves as a quasi-standard. There is a proposal
for a GraphQL over HTTP specification2 , but it is not yet official at the time of writing.
Transport protocols are handled at the Spring for GraphQL level. GraphQL Java does
not dictate any transport protocol.
JSON is the most common serialization choice. Although the GraphQL spec does not
dictate any particular serialization format, given the sheer popularity of JSON, the spec
includes a section on JSON serialization.
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>"
}
1
https://graphql.org/learn/serving-over-http
2
https://github.com/graphql/graphql-over-http
98
Request and response
The goal is to execute exactly one GraphQL operation. The HTTP endpoint is always
the same, often ending with /graphql by convention.
In the request body, the first key “query” is actually a GraphQL document, rather than
a GraphQL query. A GraphQL document can contain one or many operations, and is
represented in the request as a JSON string. This key is not optional.
The next key “variables” is a JSON map with all the variables for the operation. This
key is optional, as operation variables are optional.
The third key “operationName” specifies which operation to execute in the document.
This is optional and only required when there are multiple operations in the document.
Under the hood, the transport protocol is handled by Spring for GraphQL. An HTTP
request in Spring for GraphQL is represented by a WebGraphQlRequest containing
HTTP-specific information such as headers.
And at the GraphQL Java level, a GraphQL request is represented by a
graphql.ExecutionInput instance. Here are the most important fields:
query represents the “query” key in the JSON object, and operationName represents the
“operationName” key. rawVariables represents the variables map in the JSON object.
The word raw reflects that the variables have not yet been coerced. We’ll discuss variable
coercion in the Execution chapter.
Note that transport concerns are managed at the Spring for GraphQL level. GraphQL
Java’s ExecutionInput does not specify any transport protocol. The most important
fields in this class correspond to the JSON object in the request body we saw previously.
Response
{
"data": <data>,
"errors": <list of GraphQL errors>,
"extensions": <map of extensions>
}
99
Request and response
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.
The response’s HTTP status code depends on whether the GraphQL Java engine was
invoked.
If the request is rejected before the GraphQL Java engine is invoked, we use HTTP
status codes to indicate the problem. For example, a 401 Unauthorized code is returned
for authentication failures, or 400 Bad Request if the request itself is not a GraphQL
request (e.g. the body is missing a “query” key).
If the request is rejected after the GraphQL Java engine is invoked, the 200 OK status
code is always returned. Any errors are represented as GraphQL errors in the JSON
response body.
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:
100
Request and response
This is a gotcha for developers with REST API experience. To determine whether a
request succeeded, remember to check both the HTTP status code and the errors key of
a response.
HTTP headers
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.
We must have the customer ID to make sense of the request. It is much better to explicitly
require the customer ID as an argument in the query request.
type Query {
ordersByCustomerId(id: ID!): [Order]
}
A person reading this improved schema would easily understand that there must be an
id argument provided to fulfil the request. As the id argument is non-nullable, GraphQL
validation and tooling will also require the argument be provided to proceed with the
request.
We recommend sending information needed to understand the request in the GraphQL
operation. However, that doesn’t mean request headers can’t be used at all. You should
continue to use request headers to send meta or auxiliary data, which is not necessary to
understand the GraphQL request.
One prominent example is sending authentication information. Although authentication
is required to execute the request, we can understand the intent of the request without
seeing credentials.
101
Request and response
Another example is sending beta flags via request headers. If the client wants to use
certain fields that are not yet stable, we could require some special header such as
“beta-features”. With or without this beta flag information, the request on its own make
sense.
A third example is tracing information such as request IDs. Metadata like request IDs
are useful for logging but are not essential to understand the GraphQL request.
Intercepting requests
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@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;
});
102
Request and response
}
}
Another use case is to change execution based on a request header. In this example,
clients are required to send a special beta flag “beta-features” to request beta fields which
are not yet stable. In the interceptor, if this “beta-features” header is present, we add
“beta-features” to the GraphQLContext so this information can be accessed later in the
execution. We’ll cover GraphQLContext in more detail in the DataFetchers in depth
chapter.
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
class BetaFeaturesInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request, Chain chain
) {
boolean betaFeatures = request
.getHeaders()
.containsKey("beta-features");
return chain.next(request);
}
The example above changed the GraphQL Java request object ExecutionInput. We
can also access and change the entire GraphQL Java response ExecutionResult. In the
103
Request and response
package myservice.service;
import graphql.ExecutionResult;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@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
ExecutionResult executionResult = response.getExecutionResult();
Map<Object, Object> newExtensions = new HashMap<>();
if (executionResult.getExtensions() != null) {
newExtensions.putAll(executionResult.getExtensions());
}
// Replace value with your request ID mechanism
newExtensions.put("request_id", "YOUR_REQUEST_ID_HERE");
104
Request and response
In this chapter we discussed requests and responses for GraphQL in greater detail, and
demonstrated how to access these objects in Spring for GraphQL.
In the next chapter, we’re going to build on these concepts and discuss GraphQL errors.
105
GraphQL errors
What happens when things go wrong? How do we communicate errors from our GraphQL
service?
In the previous chapter we started discussing GraphQL responses, which are JSON
objects with three key entries: data, errors, and extensions.
{
"data": <data>,
"errors": <list of GraphQL errors>,
"extensions": <map of extensions>
}
In this chapter we’ll discuss GraphQL errors in detail. We will discuss how errors are
presented to the client and how to customise GraphQL errors in our Spring for GraphQL
service.
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.
Some examples of request errors1 from the GraphQL spec are:
106
GraphQL errors
query invalid {
{
foo
}
}
The response contains an error, including a message indicating what went wrong:
{
"errors": [
{
"message": "Invalid Syntax : offending token '{'
at line 2 column 5",
"locations": [
{
"line": 2,
"column": 5
}
],
"extensions": {
"classification": "InvalidSyntax"
}
}
]
}
Every error must contain the key message, with a description of the error. In this case,
the message indicates the request contained invalid GraphQL syntax. If the error can
be linked to a location in the GraphQL document, it should be presented to make the
error easier to find. The location information indicates the invalid syntax is at line 2 and
column 5 of the GraphQL document.
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.
107
GraphQL errors
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.
Let’s see another example of a request error, when we attempt to request a field
doesNotExist that does not exist in the Query type of the schema.
query missing {
doesNotExist
}
{
"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.
In the extensions map, GraphQL Java inserts the classification ValidationError. It
is invalid to ask for a field in a GraphQL request which does not exist in the schema.
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.
For example, a basic Pet schema with friends.
108
GraphQL errors
type Query {
favoritePet: Pet
}
type Pet {
id: ID
name: String
friends: [Pet]
}
query whoIsAGoodPup {
favoritePet {
name
friends {
name
}
}
}
package myservice.service;
import java.util.List;
@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);
}
109
GraphQL errors
@SchemaMapping
List<Pet> friends(Pet pet) {
throw new RuntimeException("Something went wrong!");
}
}
The DataFetcher that loads the friends of the pet throws an exception. This is the
GraphQL response for the whole query, in JSON.
{
"errors": [
{
"message": "INTERNAL_ERROR for f8f26fdc-4",
"locations": [
{
"line": 4,
"column": 9
}
],
"path": [
"favoritePet",
"friends"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"favoritePet": {
"name": "Luna",
"friends": null
}
}
}
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
110
GraphQL errors
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.
In the extensions key, Spring for GraphQL inserts the classification INTERNAL_ERROR.
We’ll expand on error classifications added by Spring for GraphQL later in this chapter.
To recap, a GraphQL response contains data, errors, and extensions. The response
is returned as a JSON object. In GraphQL Java, this response is represented as an
ExecutionResult, containing data, a java.util.List of graphql.GraphQLError ob-
jects, and a java.util.Map of extensions.
{
"data": <data>,
"errors": <list of GraphQL errors>,
"extensions": <map of extensions>
}
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.
The extensions entry is optional and there are no restrictions on the contents of this
map.
In GraphQL Java, an individual error is represented by the graphql.GraphQLError
interface, which contains these methods.
111
GraphQL errors
String getMessage();
List<SourceLocation> getLocations();
List<Object> getPath();
Map<String, Object> getExtensions();
ErrorClassification getErrorType();
The GraphQL spec defines an error can contain up to four keys: message, locations,
path, and extensions. While the first four methods directly represent keys in the
JSON response, ErrorClassification is a GraphQL Java-specific interface that allows
us to classify an error. This is what appears in the classification field inside the
extensions map of the GraphQL response.
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.
GraphQL Java includes common error classifications, and Spring for GraphQL adds a
few additional classifications. You can also create custom classifications.
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
Additionally, Spring for GraphQL adds a few more useful error classifications, which also
implement the graphql.ErrorClassification interface.
112
GraphQL errors
• BAD_REQUEST
• UNAUTHORIZED
• FORBIDDEN
• NOT_FOUND
• INTERNAL_ERROR
You can also create custom error classifications by implementing the ErrorClassification
interface from GraphQL Java. Error classifications are added to the GraphQL error
builder, as we’ll see later in this chapter.
For example, you could categorize different types of failed authorization checks, rather
than the catch-all “UNAUTHORIZED” error classification. You can create separate
error classifications for each category of authorization failure, and use these categories to
monitor authorization failures with metrics on the server side.
We’ve seen how errors are presented in GraphQL responses. Now let’s discuss how to
return errors inside our Spring for GraphQL service. We’ll discuss two fundamental ways
to return field errors: by throwing exceptions during a DataFetcher invocation or by
returning errors via DataFetcherResult.
113
GraphQL errors
One way to return errors is to raise an exception during DataFetcher invocation. When
a DataFetcher throws an exception, GraphQL Java converts it into a GraphQL error,
which is added to the overall GraphQL response and the value of field is set to null.
In our earlier example where the friends DataFetcher raised a RuntimeException, the
friends field in the data was set to null. In a more realistic service, the friends
database may be unavailable, causing an exception to be raised.
When an exception is thrown during DataFetcher invocation, GraphQL Java’s
DataFetcherExceptionHandler is called. A GraphQL Java application can register a
DataFetcherExceptionHandler, and Spring for GraphQL provides a built-in handler
ExceptionResolversExceptionHandler. This handler allows for a chain of exception
resolvers to be registered. This is configured for use by the Spring Boot starter. As
we saw in the error examples earlier in this chapter, no additional code is required to
configure exception handling.
However, if you would like to customize exception resolution, you can register
DataFetcherExceptionResolvers. Spring for GraphQL makes it easy to register
custom exception resolvers, which we’ll demonstrate in the next section on customizing
exception resolution.
Even if you have not explicitly selected Spring WebFlux as a dependency for your
GraphQL service, internally Spring for GraphQL uses Reactor types such as Mono. Spring
for GraphQL uses reactor-core as a dependency. Note that while Reactor is used
internally by Spring for GraphQL, you do not need to use Reactor types in your code
for your service. If the words Mono and Flux are unfamiliar, it is fine to skip over the
discussion of Reactor types in the Spring for GraphQL internals.
Each registered exception resolver will be called, one after another, until one returns a
list of GraphQL errors (which can be empty). If the returned Mono from the exception
resolver completes empty, without emitting a list, the exception remains unresolved, and
we invoke the next exception resolver.
If no exception resolver takes care of the exception, Spring for GraphQL creates a default
error with an INTERNAL_ERROR classification with a generic error message. Spring for
GraphQL makes an intentionally opaque message to avoid leaking implementation details.
In general, we suggest you only show clients what they need to know to understand the
response. Avoid revealing implementation details. For example, avoid dumping stack
traces in the error message. You should separately monitor detailed exception information
such as stack traces.
114
GraphQL errors
If you would like to customize exception resolution, Spring for GraphQL offers a
DataFetcherExceptionResolverAdapter abstract class that already implements much
of the contract for you.
For example, let’s implement an exception resolver that overrides the actual exception
message.
package myservice.service;
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 classification
.message("My custom message") // Overrides the message
.build();
}
}
115
GraphQL errors
The only way to return both data and errors for a field is with DataFetcherResult.
Contrast this to throwing exceptions, which will always set the field value to null.
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;
import java.util.List;
116
GraphQL errors
package myservice.service;
import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.GraphqlErrorBuilder;
import graphql.execution.DataFetcherResult;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@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));
In this chapter, we discussed GraphQL errors in depth. We discussed request and field
errors, and how they are presented in the GraphQL response. Then we demonstrated
how to raise errors in our Spring for GraphQL service, and how to customize exception
handling.
117
Schema design
118
Schema design
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String
}
Then we might change the schema to include address information for a user like this.
type Query {
user(id: ID!): User
}
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.
It’s more challenging to evolve the schema when a change is a breaking change. For
example, if we decide that the current name: String is not enough, we introduce a better
UserName object with fields like legalName and preferredName.
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: UserName
119
Schema design
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.
This is how to manage breaking changes:
Let’s walk through how we would manage a breaking change in our User example.
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
}
120
Schema design
Then we deprecate the old field with the built-in @deprecated directive. Note that
directives appear after the declaration that they decorate such as name.
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String
@deprecated(reason: "Use richer alternative `userName`.")
userName: UserName
}
type UserName {
legalName: String
preferredName: String
}
The next step is very context specific. Depending on the kind of client API and any
service guarantees, we might monitor its usage and wait until nobody uses the field
anymore. Or we might simply give all clients a certain amount of time to migrate, such
as 6 months, or do a combination of both.
Then in the last step, remove the field.
type Query {
user(id: ID!): User
}
type User {
id: ID!
userName: UserName
}
type UserName {
legalName: String
preferredName: String
}
Now we have finished the gradual transition from name: String to userName:
UserName.
Of course, you should adjust your approach based on your situation.
121
Schema design
For example, you may choose to immediately make a breaking change if:
You might also choose to retain a deprecated field for the foreseeable future rather than
removing the field.
Our other general recommendation for these kinds of breaking changes is to design the
schema for evolution and avoid them as much as possible. This chapter covers specific
recommendations for schema evolution.
Every production GraphQL API is bound to face breaking changes. The general goal is
to minimize the amount of breaking changes, not to avoid them completely.
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.
For example, this schema is not connected.
type Query {
issue: Issue
userById(id: ID!): User
}
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.
122
Schema design
# First
query myIssue {
issue {
ownerId
}
}
# returns "123"
# Second
query myUser {
userById(id: "123") {
name
}
}
type Query {
issue: Issue
}
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
}
}
}
It is good to look for any <name>Id: String/ID or other unique identifier fields in a
schema, then verify if there is a good reason to retain them, or whether you might directly
connect them to some other data.
123
Schema design
Of course, there are limitations to how connected something can be, depending on the
data sources you have access to.
Don’t make types and fields overly generic in an attempt to make them reusable.
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
124
Schema design
direct negative effects with a larger amount of fields. We should make single-purpose
fields explicit with specific naming.
Compare these two examples:
type Query {
pet(id: ID, name: String): Pet
}
vs
type Query {
petById(id: ID!): Pet
petByName(name: String!): Pet
}
The second version is better in every aspect. We have better names and the arguments
are marked as non nullable. We can also again evolve the schema much more easily.
Nullable fields
One of the most misunderstood topics in schema design is the nullability of fields. When
starting to learn GraphQL, it confuses many people that fields are nullable by default,
which leads to beginners making almost all fields non-nullable.
In GraphQL, almost all fields should be nullable to allow results to return more data.
Let’s take a closer look at how GraphQL handles null data through examples.
Consider the case where a field is marked as non-nullable, but the data is null during
execution. The schema gives the assurance that the field is never null, so GraphQL
cannot return null. Instead, the parent is set to null if possible. If the parent of the
original field is also non-nullable, then we set the parent of the parent to null if possible,
and so on. The error is propagated through the hierarchy of parent fields until a field
can be set to null.
Let’s step through null result handling with a concrete schema example:
type Query {
a: A
}
type A {
b: B
125
Schema design
type B {
c: C!
}
type C {
d: String!
}
query myQuery {
a {
b {
c {
d
}
}
}
}
{
"data": {
"a" : {
"b": null
}
}
}
d was null, but marked as non-nullable. Therefore, we tried to set the parent c to null.
But c is also non-nullable, therefore we try and successfully set b to null.
If we change the schema so that field b: B! can’t be null either, then we end up with
this.
{
"data": {
"a" : null
}
}
126
Schema design
And finally, if we change field a to a: A!, then we end up with everything set to null.
{
"data": null
}
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.
A pet and human schema:
type Query {
pet: Pet!
human: Human
}
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
}
127
Schema design
This is because we marked pet as non-nullable and the error propagates up and wipes
out all results.
Now that we understand how GraphQL handles non-null, we can rephrase what it means
that we declare a field non-null: “A non-nullable field is so essential that all other fields
make little sense without it”.
The most basic examples are id fields. Normally, if the fetching of the id is unsuccessful,
we can’t guarantee anything else and therefore we should make it non-nullable.
type User {
id: ID!
name: UserName
address: Address
}
In this example, name and address are not as fundamental and therefore not declared
non-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 Query {
orders: [Order]
}
type Order {
id: ID!
customer: Customer!
# And more order fields here
}
128
Schema design
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.
If we look at customer again from the angle of “which fields are essential”, then we
realize that non-nullable isn’t a good choice either. Other order data (which we have not
shown in the example) might be still valuable, even when we can’t load the customer.
One special case where non-nullable fields often make sense is inside lists. For example:
type Query {
orders: [Order!]
}
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.
To summarize the recommendations about non-nullable Fields:
Input fields are nullable by default, but in practice, we usually want to make as many of
them non-nullable as possible. Non-nullable input fields and arguments have the simple
advantage of clearly communicating that we need this input to the API user, while forcing
us to be specific about the use case.
A nullable input field or argument often signals that we might have a field that is too
generic, and we should think about how we can make them non-nullable.
129
Schema design
type Query {
user(id: ID, name: String): User
}
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.
Consider the counterexample where a name field inside the input type has a special
behavior for null, to indicate the user ought to be deleted.
input UpdateUserInput {
id: ID!
name: String # null indicating deletion of the user
}
This is not a good solution because it’s harder for a user to understand. It is much better
to split into two inputs.
input ChangeUserInput {
id: ID!
name: String!
}
input DeleteUserInput {
id: ID!
}
As with fields, the elements inside a list are often an excellent good candidate for making
non-nullable.
We strongly recommend that all lists use pagination, unless the list size is small and
limited based on the domain.
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.
A list of hundreds of elements can cause a noticeable slowdown in page loading time, due
to the sheer time required to send the response back to the client. As the list continues
to grow in size, it will become infeasible for a service to send all list elements before the
connection timeout, or before the user’s patience runs out.
130
Schema design
We strongly recommend that all lists use pagination unless their size is very small and
it is very clear that the list can’t grow further based on the domain. For example, the
number of planets in the Solar System is limited, so planets would not need pagination.
Based on our experience we recommend considering pagination for lists larger than 25-50
elements.
The most common pagination approach in GraphQL comes from Relay’s cursor connec-
tions specification1 , and has become the de facto standard for how GraphQL schemas
should handle large lists. Relay is a JavaScript framework for fetching GraphQL in React
applications.
The Relay connections specification is a “cursor based pagination”, meaning requests
are slices of the overall list relative to a “cursor”. This cursor identifies the position
within the overall list where we start a slice from. Then we pick another cursor and slice
again. We can slice backwards or forwards. Notably, we can’t skip any elements, we must
request a fresh slice relative to a cursor.
The Relay connections specification implements cursor based pagination with a few
concepts: Connections, Edges, Nodes, and PageInfo. We’ll explain these concepts
alongside an example Pet schema.
While there is quite some ceremony around pagination, the effort is worthwhile in order
to produce a good API.
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 {
1
https://relay.dev/graphql/connections.htm
131
Schema design
cursor: String!
node: Pet!
}
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type Pet {
name: String
# Your additional Pet fields here
}
This is how to query the first 2 pets with a schema implementing the Relay connections
specification.
query myPets {
pets(first: 2) {
edges {
cursor
node {
name
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
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.
132
Schema design
An edge is a wrapper around the actual entity we want to iterate over, in this example a
node representing a Pet. The name in this query is the name field of a Pet. Edges also
provide metadata such as cursor.
At the same level of edges, we also query pageInfo for general information about the
results, so can we move forward or backward in the list.
An example response to the query could look like this.
{
"pets": {
"edges": [
{
"cursor": "ABCD123",
"node": {
"name": "Luna"
},
{
"cursor": "XYZ789",
"node": {
"name": "Skipper"
}
],
"pageInfo": {
"startCursor": "ABCD123",
"endCursor": "XYZ789",
"hasNextPage": true,
"hasPreviousPage": false
}
}
}
In our initial query, we received data for two pets with their corresponding cursors. In
the response, hasNextPage was true, so we can request the next slice of data.
To request the next 10 pets after “Skipper” (which had a cursor of “XYZ789”), we would
query:
query morePets {
pets(first: 10, after: "XYZ789") {
edges {
cursor
133
Schema design
node {
name
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
We could also go backwards, and request the one pet before “Skipper” (which had a
cursor of “XYZ789”):
query previousPet {
pets(last: 1, before: "XYZ789") {
edges {
cursor
node {
name
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
We previously walked through how to use pagination as a client. Now let’s take a closer
look at how to model pagination in our schema.
Connection
134
Schema design
Instead of returning a list, pagination means we return a connection object with the
name <Entity>Connection, such as PetConnection in our example.
type Query {
pets(
first: Int,
after: String,
last: Int,
before: String
): PetConnection
}
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.
Note that you can choose to support only first/after or last/before, if you don’t
want to support pagination in both directions.
You could also add more arguments to filter elements, such as a namePattern to filter
pets by name:
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!
}
An edge is a wrapper containing the element data and additional metadata. An edge
type is named <Entity>Edge. In our example, we are interested in pages of Pets so call
this type a PetEdge.
Pagination metadata is always called PageInfo and shared across all edges.
135
Schema design
We can add more fields to a connection type. For example, a connection type could
contain a totalCount field. Although, note that adding totalCount can be problematic
because it might not be easy to support when the underlying architecture changes.
Edges
type PetEdge {
cursor: String!
node: Pet!
}
PageInfo
The PageInfo type contains pagination metadata summarizing all the requested edges
in a query. It must contain the following fields:
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
PageInfo summarizes the current page’s location and whether additional data can be
requested before or after the current page.
You might have noticed in previous examples, we had a cursor field for every element
inside the node field, as well as a summary of cursor information in the PageInfo type.
If you are only interested in cursor information for the overall page, and not for each
individual element, you can use a shortcut to shorten your query.
You can add a direct nodes field on a connection type.
136
Schema design
type PetConnection {
edges: [PetEdge]
nodes: [Pet] # Optional shortcut
pageInfo: 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
}
}
}
Compare the query above to the initial pagination example in this section, which is longer
because it queries pet data with nodes via the edges field.
query myPets {
pets(first: 2) {
edges {
cursor
node {
name
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
137
Schema design
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.
Some typical examples of expected errors:
If the client wants to react to these situations, GraphQL errors are not great because
they exist outside the normal response data and are untyped.
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.
Imagine a more complex query where the response contains partial data and some errors.
It would be even harder to parse and handle the error correctly.
These shortcomings of GraphQL errors led to the idea of modeling expected errors in the
schema. This makes it part of the typed API contract and allows a client to handle them
much more safely. The cost we have to pay is a slightly more complex schema, as we will
see in the next example.
Example:
138
Schema design
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"
}
}
}
For mutations, the Payload type is a natural place for mutation-specific errors. For
queries, we can use a union type to allow for normal results or errors.
type Query {
pet(id: ID!): PetLookup
}
type Pet {
# Your Pet fields here
}
type PetLookupError {
# Your PetLookupError fields here
}
139
Schema design
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 Relay2 .
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload
}
Naming standards
The GraphQL community has largely come to a consensus on schema naming standards.
It’s good to adhere to these standards to build consistent schemas that also align with
the overall GraphQL community.
2
https://relay.dev/docs/v1.6.2/graphql-server-specification/#mutations
140
Schema design
You might have noticed we have followed these standards throughout the book.
In this chapter we covered key principles and best practices from our experiences running
GraphQL services. We hope this chapter helps you design your own production ready
GraphQL schemas.
141
DataFetchers in depth
In this chapter we will build on the earlier DataFetchers chapter and discuss more
advanced details, including how to make use of global and local context, and reactive
patterns.
In the first DataFetchers chapter we discussed inputs to Spring for GraphQL’s schema
mapping controller methods, such as source (parent) objects, arguments, and more. In
this section, we’ll discuss two additional inputs, global context and local context.
Global context
@SchemaMapping
MyType myField(GraphQLContext context) {
context.put("userId", 123);
// Your logic here
}
142
DataFetchers in depth
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@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;
});
return chain.next(request);
}
Retrieve the userId value by using the @ContextValue parameter, which retrieves a
specific value from the GraphQLContext in schema mapping or batch mapping handlers.
@SchemaMapping
MyType myField(@ContextValue String userId) {
// Your logic here
}
Note: the capitalisation of “L” is slightly different for the getter in DataFetchingEnvironment.
143
DataFetchers in depth
Local context
It’s also possible to set local context which only provides data to child DataFetchers,
rather than changing global context.
A GraphQL request is a tree of fields and every field has an associated DataFetcher. Child
DataFetchers are only invoked after the current DataFetcher finishes. We’ll discuss the
tree of fields concept in more detail in the Execution chapter. Therefore, we can ensure
that information set in local context will only be made accessible to child DataFetchers.
We can set the local context by returning a new DataFetcherResult where localContext
is not null. We discussed how DataFetcherResult can be used to return data and errors
in the Errors chapter. Now we’ll show how it can also be used to set the local 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
}
144
DataFetchers in depth
type Person {
name: String
}
query customerDetails {
customerById(id: "ID-1") {
contact {
name
}
}
}
or
query orderDetails {
order {
customer {
contact {
name
}
}
}
}
package myservice.service;
package myservice.service;
package myservice.service;
145
DataFetchers in depth
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.
In Spring for GraphQL, local context must be a GraphQLContext object, set by returning
a DataFetcherResult in the Query.order DataFetcher. Note that this local instance of
GraphQLContext is local to a DataFetcher and its children, it is different to the instance
of GraphQLContext available globally.
We can retrieve the local context from the DataFetchingEnvironment. If the Person
is in the context, we can reuse the information. Otherwise, we’ll make a request to the
Person service for contact information.
In the example below, we have used placeholder data retrieval methods. Replace these
methods with your logic. If you prefer to quickly test this end-to-end, you can mock
these methods with in-memory objects.
package myservice.service;
import graphql.GraphQLContext;
import graphql.execution.DataFetcherResult;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@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
146
DataFetchers in depth
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);
}
In this example, the Person is not guaranteed to be set in the local context. If a query
is for customer details only, without an order, there will be no personForContact in the
local context. As we cannot be sure if personForContact will be in the local context,
we must access the local GraphQLContext via the DataFetchingEnvironment and then
check if a Person has been set under the personForContact key.
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.
In pure GraphQL Java, the local context is also set via a DataFetcherResult in the
Query.order DataFetcher. If using GraphQL Java without Spring for GraphQL, the
object inserted into local context can be of any type, and does not have to be an instance
of GraphQLContext.
147
DataFetchers in depth
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.
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.
148
DataFetchers in depth
Non-reactive DataFetcher
@Controller
record ThingController(DoSomeThing service) {
@SchemaMapping
MyType myField() {
return service.doSomething();
}
}
DoSomeThing service;
DataFetcher<MyType> myField = (env) -> {
return service.doSomething();
};
This is the most straightforward pattern. If you are developing a non-reactive service,
this is the only pattern you need.
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.
If the DataFetcher involves blocking I/O, we can offload the blocking call to another
thread.
In pure GraphQL Java, it could look like:
Executor threadPool;
DataFetcher<CompletableFuture<MyType> df = (env) -> {
return CompletableFuture.supplyAsync(
() -> client.blockingCall(),
threadPool);
};
149
DataFetchers in depth
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.
In Spring for GraphQL, we recommend using a reactive approach with Reactor rather than
using Java’s CompletableFuture. As we’ll see later in this chapter, Reactor DataFetchers
in Spring for GraphQL are available without any additional code or configuration. Using
Reactor DataFetchers also enables the use of Reactor context.
Reactive I/O
A reactive DataFetcher usually involves using a reactive library such as Async Http
Client1 or Spring WebClient2 . While the library details may vary, essentially it involves
calling the library and returning a CompletableFuture.
ReactiveClient client;
DataFetcher<CompletableFuture<Something> df = (env) -> {
return client.call();
};
This pattern should be used in a reactive service every time I/O is involved.
Reactive compute work requires a bit more effort compared to the previous example, as
it requires offloading the actual work onto another thread.
The main pattern looks like this:
Executor threadPool;
DataFetcher<CompletableFuture<Something> df = (env) -> {
return CompletableFuture.supplyAsync(
() -> client.call(),
threadPool);
};
This ensures that the compute-intense work is completed in a separate thread, so it does
not block GraphQL Java.
In Spring for GraphQL, we recommend using a reactive approach with Reactor rather
than using Java’s CompletableFuture, we’ll discuss this later in this chapter.
1
https://github.com/AsyncHttpClient/async-http-client
2
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reacti
ve/function/client/WebClient.html
150
DataFetchers in depth
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.
The main tradeoff is between improved scalability or more complicated code. A reactive
service is more stable and predictable under load than a non-reactive one. If you are
going to run a service with a high load that also needs to be very stable, reactive is our
recommendation.
However, it comes with the cost of maintaining and running a reactive code base. Reactive
is not a concept inherent to the Java language itself. The concept was added later via
CompletableFuture and other libraries such as Reactor. The cost is code that is not as
simple to read, write, and debug as “normal” Java code.
A critical consideration is that everything must be reactive in order to achieve the full
benefits of a reactive code base. This means that if your HTTP client inside a DataFetcher
is not reactive, you can’t make that DataFetcher fully reactive. The following example
may seem to make sense at first glance, but it is not a suitable solution.
RestTemplate restTemplate;
URI url;
Executor threadPool;
This DataFetcher returns a CompletableFuture, but it actually does a blocking call via
the Spring RestTemplate3 HTTP client, which is a blocking call. Whilst this does not
block the GraphQL Java engine directly, there is still a thread being blocked by the
.getObject call. This means we will not achieve the full benefits of a reactive service.
3
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client
/RestTemplate.html
4
https://projectreactor.io/docs/core/release/reference/index.html#intro-reactive
151
DataFetchers in depth
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 documentation5 .
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.
No additional code nor configuration is required to make use of Reactor. Simply write a
DataFetcher that returns a Reactor type. For example:
or
To see more examples of DataFetchers returning Reactor types, see the examples in the
Building a GraphQL service chapter.
One challenge when using Reactor with GraphQL Java is that GraphQL Java itself
is based on CompletableFuture. Spring for GraphQL manages conversion between
Reactor types and CompletableFuture. To prevent the Reactor Context6 from being
lost between conversions to and from CompletableFuture, Spring for GraphQL saves
and restores the Reactor context across different DataFetcher invocations.
For example, if we want to propagate a logging prefix via Reactor context:
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
5
https://projectreactor.io/docs/core/release/reference/index.html#core-features
6
https://projectreactor.io/docs/core/release/reference/#context
152
DataFetchers in depth
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request,
Chain chain) {
return chain.next(request)
.contextWrite(Context.of("loggingPrefix", "123"));
}
For more details about WebGraphQlInterceptor, see the intercepting requests section
in the Requests chapter.
Every DataFetcher and any other code called by a DataFetcher can access the prefix.
@QueryMapping
Mono<String> foo() {
return Mono.deferContextual(contextView -> {
String loggingPrefix = contextView.get("loggingPrefix");
return Mono.just(loggingPrefix);
});
}
In this chapter we covered more advanced details about DataFetchers, including how to
make use of global and local context, and reactive patterns. We also discussed how to
use Reactor types with Spring for GraphQL.
153
Directives
Directives are a powerful feature of GraphQL that allows us to declare any kind of
additional data to a schema or document. This data can be used to change runtime
execution or type validation behavior.
In this advanced chapter, we will discuss directives in depth and demonstrate use cases
for directives.
There are two broad categories of directives, schema and operation directives. Schema
directives are used on schema elements, and operation directives are used in operations
within a GraphQL document.
Schema and operation directives have a name starting with @, followed by an optional
list of arguments in parentheses. For example, here is the built-in @deprecated schema
directive which is used to indicate deprecated schema elements.
type Query {
search: String @deprecated(reason: "Too slow, please use searchFast")
searchFast: String
}
Schema and operation directive definitions have a name starting with @, an optional list
of arguments in parentheses, followed by on and a list of allowed locations where it can
be used, separated by |. Schema and operation directive definitions are declared in the
schema.
154
Directives
The allowed locations will determine whether this directive is a schema or operation
directive. For example, a directive on a FIELD_DEFINITION is a schema directive, whereas
a directive on a FIELD is an operation directive. We’ll discuss the full list of locations for
schema and operation directives later in this chapter when we create our own directives.
The GraphQL spec defines four built-in directives: @skip, @include, @deprecated and
@specifiedBy. @skip and @include are operation directives, whereas @deprecated and
@specifiedBy are schema directives. You can also create your own schema and operation
directives, which we’ll cover in this chapter.
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.
@skip and @include are operation directives that allow us to skip or include certain
fields during execution.
You should not declare these built-in directives in your schema. To illustrate how they
can be used, this is how @skip and @include are defined:
query myPets {
pets {
name
}
}
# same as:
155
Directives
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:
@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.
As @deprecated is a built-in directive, you should not declare it in your schema. To
illustrate how @deprecated is used, this is how it is defined:
You can optionally provide a reason for deprecation, which will appear in automatically
generated documentation and tooling. The default reason is “No longer supported”.
This is an example of how to mark fields and enums as deprecated in the schema:
156
Directives
type Query {
search: String @deprecated(reason: "Too slow, please use searchFast")
searchFast: String
}
enum Format {
LEGACY @deprecated(reason: "Legacy format")
NEW
}
157
Directives
@specifiedBy
scalar DateTime
And this is still a valid way to define custom scalars in a schema. However, only a name
in the schema is not enough to explain the behaviour of custom scalars. For example,
DateTime implementations can vary across services, but they might both contain a schema
element with the same name DateTime. The @specifiedBy directive was introduced
later to provide a way to clearly document the behavior of custom scalars. The provided
URL should link to a specification including data format, serialization, and coercion rules.
For the full details and specification templates, see the GraphQL Scalars project1 .
We recommend using @specifiedBy to clearly describe your custom scalar. However,
using this directive with custom scalars is not compulsory.
As @specifiedBy is a built-in directive, you should not declare it in your schema. To
illustrate how @specifiedBy is used, this is how it is defined:
With the GraphQL Scalars2 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 Scalars3 project for more information.
1
https://scalars.graphql.org/
2
https://scalars.graphql.org/
3
https://scalars.graphql.org/
158
Directives
Schema and operation directives have a name starting with @, an optional list of arguments
in parentheses, followed by on and a list of allowed locations where it can be used. The
allowed locations determine whether the directive is an operation directive or schema
directive. When creating your own schema and operation directives, they must be defined
in the schema.
It’s important to understand that all custom schema and operation directives
don’t have any effect until we implement the custom behavior. We’ll first discuss
how to define schema and operation directives, then how to implement them.
Let’s walk through some examples. Let’s create an @important directive. The directive
can only be used on field definitions, which makes it a schema directive:
Our new @important directive can be used to indicate certain schema fields are impor-
tant.
type Query {
hello: String @important # usage of the directive
}
We have defined the @important directive to only be allowed on fields inside the schema.
Other locations will be invalid, for example:
By using the directive on the Query type, we have created an invalid schema.
To make this a valid schema, we could add another location to the directive definition.
Provide multiple locations by separating them with |.
159
Directives
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.
The difference between schema directives and operation directives is the list of allowed
locations. Here is an example of a schema directive @foo with all possible eleven locations
in a schema.
160
Directives
Whilst it is technically possible to define a directive that includes locations for both
schema and operation directives, in practice this is not common.
Let’s define an operation directive @cache, which can be used on operation fields. Note
that FIELD in the example below refers to operation fields, and FIELD_DEFINITION used
in the previous schema directive example refers to schema fields.
type Query {
pet: Pet
}
type Pet {
name: String
lastTimeOutside: String
}
We can only use this @cache directive on fields in a GraphQL document, which contains
operations.
query myPet {
pet {
name
lastTimeOutside @cache
}
}
Directives can also have arguments. Let’s add a maxAge argument, with a default value
of 1000.
161
Directives
In a GraphQL document, we could use our updated @cache directive to specify a maxAge
value:
query myPet {
pet {
name
lastTimeOutside @cache(maxAge: 500)
}
}
All custom schema and operation directives don’t have any effect until we implement
new custom behavior. For example, the operation above where lastTimeOutside has
a @cache directive behaves exactly the same as without it, until we have implemented
some new logic. We’ll demonstrate implementation of behavior for directives later in this
chapter. You don’t need to define behaviour for the built-in directives, which all have a
well-defined effect that is implemented by every GraphQL implementation.
The difference between schema directives and operation directives is the list of allowed
locations. Here is an operation directive with all possible eight locations in a GraphQL
document, which contains operations.
query someQuery(
$var: String @foo # Variable definition
) @foo # Query
{
field @foo # Field
... on Query @foo { # Inline fragment
field
}
...someFragment @foo # Fragment spread
}
162
Directives
Repeatable directives
type Query {
# Multiple owners per field possible
hello: String @owner(name: "Brian") @owner(name: "Josh")
}
To create a new schema directive, we have to define the directive and implement the
logic for it.
As this is an advanced chapter, the code examples which follow are more complicated
and involve schema traversal and transformation.
Let’s implement the logic for a new @important directive, which indicates which schema
fields are important, and the reason for its importance. It is defined as:
163
Directives
type Query {
hello: String @important(reason: "Being friendly")
}
To explain how schema directive definition and usage are represented in code, we will
walk through sample code with pure GraphQL Java. Then we’ll wrap up this section
with an implementation for @important in Spring for GraphQL.
Schemas are represented as an instance of GraphQLSchema inside GraphQL Java. A
GraphQLSchema instance will contain a GraphQLDirective instance representing a schema
directive’s declaration.
In pure GraphQL Java, we could access this GraphQLDirective instance via
GraphQLSchema.getDirective, which contains the name, arguments, valid locations,
and whether the directive is repeatable. In this example schema is the name of an
instance of GraphQLSchema.
164
Directives
package myservice.service;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLAppliedDirective;
import graphql.schema.GraphQLFieldDefinition;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class GreetingController {
@QueryMapping
String hello(DataFetchingEnvironment env) {
GraphQLFieldDefinition fieldDefinition = env.getFieldDefinition();
GraphQLAppliedDirective important
= fieldDefinition.getAppliedDirective("important");
if (important != null) {
return handleImportantFieldsDifferently(env);
}
return "Hello";
}
}
Directives can also be used for validation. For example, a @size schema directive for
arguments, which enforces a minimum quantity.
165
Directives
type Query {
hired(applications : [Application!] @size(min : 3)) : [Boolean]
}
Validation is such a useful and commonly requested idea that there is an extended
validation library4 for GraphQL Java, which is maintained by the GraphQL Java team.
To use the validation directives in the graphql-java-extended-validation library, add the
package.
For Gradle, add this to your build.gradle file:
implementation 'com.graphql-java:graphql-java-extended-validation:19.1'
For Maven:
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-validation</artifactId>
<version>19.1</version>
</dependency>
Note: the major version number corresponds to the linked major version of the main
GraphQL Java release. At the time of writing, the latest version of Spring for GraphQL
1.1.2, uses GraphQL Java 19.
To wire these validation directives in Spring for GraphQL, create a RuntimeWiringConfigurer
bean. This will add a default selection of directive implementations from graphql-java-
extended-validation. You should separately define the directives for your service in your
schema.
@Configuration
class GraphQlConfig {
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
// Adds all default validation rules in library
ValidationRules possibleRules
= ValidationRules.newValidationRules().build();
// ValidationSchemaWiring implements SchemaDirectiveWiring
ValidationSchemaWiring validationDirectiveWiring
= new ValidationSchemaWiring(possibleRules);
return wiringBuilder -> wiringBuilder
4
https://github.com/graphql-java/graphql-java-extended-validation
166
Directives
.directiveWiring(validationDirectiveWiring);
}
}
Another very common use case of schema directives is providing metadata, which does
not change execution but is only relevant for the schema itself.
For example, we work in a large team and we want to document the ownership of certain
types with an @owner directive:
167
Directives
schemaTraverser.depthFirstFullSchema(new GraphQLTypeVisitorStub() {
@Override
public TraversalControl visitGraphQLObjectType(
GraphQLObjectType objectType,
TraverserContext<GraphQLSchemaElement> context
) {
GraphQLAppliedDirective directive
= 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);
We visit every GraphQLObjectType in the schema, and check the owner of each object
type. We then assemble a map of the owner to a list of object types.
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(..).
Taking a step further, we can even change the global GraphQLSchema with schema
directives. For example, we could automatically add a suffix to every field based on a
directive.
With pure GraphQL Java, we can make use of schema transformer and type visitor
tools.
168
Directives
@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 are using the SchemaTransformer class to change a schema whilst traversing it.
SchemaTransformer leverages the same GraphQLTypeVisitor as the SchemaTraverser
used in the previous example.
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(..).
A word of caution: as you can see from this code example, transforming a schema is
not trivial. Be careful not to inadvertently create an invalid schema during schema
169
Directives
Operation directives are used with GraphQL operations. Note that this concept is also
often referred to as “query” directives, although this type of directive can be used on all
three operations: queries, mutations, and subscriptions.
Let’s implement the logic for a @cache operation directive:
This is an operation directive that enables clients to specify how recent cache entries
must be. This is an example of an operation directive that can change execution.
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)
}
170
Directives
package myservice.service;
import graphql.execution.directives.QueryAppliedDirective;
import graphql.execution.directives.QueryAppliedDirectiveArgument;
import graphql.execution.directives.QueryDirectives;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@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();
171
Directives
172
Execution
Execution is handled by the GraphQL Java engine. In this advanced chapter, we will
look under the hood into how GraphQL Java executes a request based on a schema. By
the end of this chapter, you’ll have a deeper understanding of how requests are executed
by the GraphQL Java engine.
Before executing any GraphQL requests, Spring for GraphQL has to understand the
schema and relevant configuration. To achieve this, Spring for GraphQL initializes
an instance of GraphQL Java’s graphql.GraphQL class, which contains all the objects
needed to execute a GraphQL operation, including the schema and execution strategies.
Usually only one instance of graphql.GraphQL is initialized for all requests.
As part of the graphql.GraphQL initialization process, Spring for GraphQL loads schema
files, exposes relevant properties, detects RuntimeWiringConfigurer beans and more.
See the Spring for GraphQL documentation1 for detailed information.
While Spring for GraphQL initializes graphql.GraphQL, if there is anything you want
to change, the instance can be accessed and modified via the GraphQlSource
contract2 . For example, to add a custom execution ID provider, create a
GraphQlSourceBuilderCustomizer bean:
package myservice.service;
import org.springframework.boot.autoconfigure.graphql
.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class GraphQlConfig {
@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
1
https://docs.spring.io/spring-graphql/docs/current/reference/html/#execution.graphqlsource
2
https://docs.spring.io/spring-graphql/docs/current/reference/html/#execution.graphqlsource
173
Execution
Note that customizing the GraphQlSource is entirely optional. Spring for GraphQL
already initializes enough to start executing requests without additional custom configu-
ration.
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);
In the request and response chapter, we discussed how Spring for GraphQL handles
requests and responses over HTTP. In this chapter, we will focus on the execution steps
after a request is received. These execution steps take place inside the GraphQL Java
engine.
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.
174
Execution
For every received HTTP request, Spring for GraphQL automatically creates an in-
stance of GraphQL Java’s ExecutionInput. Recall from the request and response
chapter that ExecutionInput is the GraphQL Java object that represents a GraphQL
request, without transport concerns. Inside Spring for GraphQL, this happens in the
DefaultExecutionGraphQlService. It is possible to replace this with a custom imple-
mentation of ExecutionGraphQlService.
Control then passes to GraphQL Java, which executes the GraphQL request represented
by an ExecutionInput instance. Following execution, an ExecutionResult instance
containing response data and/or errors is returned. What happens in the GraphQL Java
engine between ExecutionInput and ExecutionResult is the focus of the remainder of
the chapter.
If you are using GraphQL Java without Spring for GraphQL, this is how to manually
create an ExecutionInput and execute the request.
Execution steps
1. parsing the “query” (document) value from the request as a GraphQL document
2. validating the document
3. coercing variables
4. fetching data
These are the steps between the GraphQL Java engine receiving a ExecutionInput
request and returning a ExecutionResult instance with data and/or errors.
Recall from the request and response chapter that the “query” in a GraphQL request is
actually a GraphQL document, which can contain one or more operations. This “query”
value is inserted into an instance of GraphQL Java’s ExecutionInput, which represents
a GraphQL request.
175
Execution
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.
If the request contains variables, they need to be “coerced”. The coercing process converts
the variable values provided in the request into an internal representation and also
validates the variables. For example:
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.
As detailed in the DataFetchers chapter, a DataFetcher in GraphQL Java is a generic
function that loads data for one specific field. Spring for GraphQL implements DataFetch-
ers via the @SchemaMapping controller annotation and shortcut annotations such as
@QueryMapping.
Every field in the schema has a DataFetcher assigned to it. DataFetchers associated with
fields in a GraphQL request will be invoked by GraphQL Java to fetch the data.
It is possible to for a field to accidentally not be mapped to any DataFetcher, even after
PropertyDataFetchers are generated. This could happen by forgetting a @SchemaMapping
annotation or missing a Java property. A forgotten DataFetcher will be treated as
176
Execution
DataFetcher returning a null result. As you usually don’t want to forget any DataFetch-
ers, the forthcoming 1.2 version of Spring for GraphQL3 will add schema mapping checks
on startup.
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
}
3
https://spring.io/blog/2023/03/21/spring-for-graphql-1-2-0-m1-released
177
Execution
}
}
GraphQL Java interprets every GraphQL operation as a tree of fields. Each field has an
associated DataFetcher. It is therefore equally valid to describe an operation as a tree of
DataFetcher closures (unnamed functions).
This query, as shown in Figure 1, has three levels, with dogs as the single root
field. GraphQL Java traverses the query breadth-first and invokes the correspond-
ing DataFetcher when visiting each field. Once a field’s DataFetcher has successfully
returned data, we invoke the DataFetcher for each of its children. So the first DataFetcher
being invoked is /dogs, followed by /dogs/name, /dogs/owner, /dogs/friends and
/dogs/details.
A critical detail is that GraphQL Java fetches the children of a field in parallel, if
possible.
For this example, let’s assume all DataFetchers allow parallel execution. The next steps
of the execution depend on the order the DataFetchers finish. Let’s say /dogs/friends
finishes first, followed by /dogs/owner then /dogs/details. This leads us to the
execution order as shown in Figure 2.
In this diagram, the numbers show the order of execution. Fields with the same number
are executed in parallel.
To summarize, the execution is a breadth-first traversal of the fields, with each field
finishing when its DataFetcher completes. We execute the children of a field in parallel,
if possible.
178
Execution
Reactive concurrency-agnostic
A key question is how much work does the DataFetcher perform in the current
thread? If no work or only very minimal work is done in the current thread, then
GraphQL Java itself works as efficiently as possible. If a DataFetcher is using the current
thread (by either doing computation, or waiting for some I/O to return), this blocks
GraphQL Java itself, which then can’t invoke another DataFetcher.
179
Execution
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.
180
Execution
We recommend that a DataFetcher should never occupy the thread it is called in, in
order to achieve maximum efficiency.
Spring for GraphQL also enables DataFetchers to return Reactor values, in addition to
CompletableFuture, as we saw in the Reactor support section in the DataFetchers Part
2 chapter.
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”.
If the value is null, completing terminates and does nothing further.
If the field type is a list, we complete all elements inside the list, depending on the generic
type of the list.
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.
For example, the built-in scalar Scalars.GraphQLInt only accepts Java Number values.
This means if we have a DataFetcher for a field of type Int and it returns the Boolean
false, it would cause an error.
type Query {
someInt: Int
}
181
Execution
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.
In our schema, we have a Pet interface:
type Query {
pet: Pet
}
interface Pet {
name: String
}
After a TypeResolver returns the type of the value, this information is used to determine
the actual sub-selection of this field that needs to be fetched.
A sample query could look like:
query myPet {
pet {
...on Dog {
barks
}
...on Cat {
meows
}
}
}
182
Execution
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 { ... }
}
DataFetchers for the mutation are invoked serially. The DataFetcher for the mutation
field addOrder is only invoked after deleteUser finishes. Likewise, the DataFetcher
for changeUser is only invoked after addOrder finishes. Contrast this behaviour to the
query where we invoke the DataFetchers in parallel for all three fields: searchUsers,
userById, and allOrders.
This is the only difference in execution between queries and mutations. We implement a
mutation as a DataFetcher, but there is a rule to follow when using GraphQL Java. A
mutation DataFetcher may have side effects, whereas queries must not, although this is
not enforced by GraphQL Java.
In this advanced chapter, we took a deep dive into execution inside the GraphQL Java
engine. We also discussed how Spring for GraphQL initializes execution objects, and how
it starts execution in GraphQL Java after receiving a request.
183
Instrumentation
Instrumentation is a general mechanism to hook into GraphQL Java. You can inject
code that can observe the execution of a query and also change runtime behavior.
Instrumentation is particularly useful for performance monitoring and custom logging.
The Spring Boot starter will automatically detect Instrumentation beans as part of the
graphql.GraphQL initialization process. graphql.GraphQL is a key GraphQL Java class
containing information necessary to execute a request.
Let’s add our first instrumentation, MaxQueryDepth. This instrumentation will detect
when an operation’s depth is above the specified number and abort execution early. You
may want to add this instrumentation to enforce a hard limit on operation depth, as a
way to limit resource use.
For example, imagine a query that requests for the names of friends, and in turn, the
names of their friends, and so on. The sheer depth of this query may result in the service
spending considerable resources to complete the request.
query veryDeep {
hero {
name
friends {
name
friends {
name
friends {
name
# And so on!
}
}
}
}
}
184
Instrumentation
For convenience, GraphQL Java includes a few built-in instrumentations, which are listed
later in this chapter. Let’s add the built-in MaxQueryDepth instrumentation as a bean to
our Spring for GraphQL service, and set the maximum depth to an appropriate number.
This instrumentation will be automatically detected and registered by the Spring Boot
starter, there is no further configuration required.
package myservice.service;
import graphql.analysis.MaxQueryDepthInstrumentation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class MyGraphQLConfiguration {
@Bean
MaxQueryDepthInstrumentation maxQueryDepthInstrumentation() {
return new MaxQueryDepthInstrumentation(15);
}
}
When an operation is requested with a depth of greater than the specified value, the exe-
cution is aborted early and the following error message is returned within the response.
{
"errors": [
{
"message": "maximum query depth exceeded 42 > 15",
"extensions": {
"classification": "ExecutionAborted"
}
}
]
}
You can write a custom instrumentation using instrumentation hooks available in the
graphql.execution.Instrumentation interface in GraphQL Java. Each hook is a
separate method. A list of available hooks is presented later in this chapter.
Let’s write a custom instrumentation that measures the time taken to execute a GraphQL
request. This is the time between the start of GraphQL execution until the time the
request is completed.
185
Instrumentation
package myservice.service;
import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentation;
import graphql.execution.instrumentation.parameters
.InstrumentationExecutionParameters;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
@Component
class LogTimeInstrumentation extends SimpleInstrumentation {
@Override
public InstrumentationContext<ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
return new InstrumentationContext<>() {
AtomicLong timeStart = new AtomicLong();
@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()));
}
};
}
}
186
Instrumentation
InstrumentationContext
InstrumentationContext is the object that will be called back when a particular step
ends. InstrumentationContext is returned by step methods in Instrumentation such
as beginExecution.
In GraphQL Java, it is represented as a simple interface.
/**
* 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 completed
*
* @ 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);
187
Instrumentation
The use of “dispatching” and “completion” reflects the reactive way GraphQL Java
is implemented internally, based on CompleteableFutures. A CompleteableFuture is
created and then it is completed later. Once the linked CompleteableFuture is created,
the engine calls the onDispatched method. When the linked CompletableFuture is
finished, the engine calls the onCompleted method.
InstrumentationContext is a generic interface, and in the previous LogTime example,
we returned an InstrumentationContext for ExecutionResult.
InstrumentationState
package myservice.service;
import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationContext;
import graphql.execution.instrumentation.InstrumentationState;
import graphql.execution.instrumentation.SimpleInstrumentation;
import graphql.execution.instrumentation.parameters
.InstrumentationCreateStateParameters;
import graphql.execution.instrumentation.parameters
.InstrumentationExecutionParameters;
import graphql.execution.instrumentation.parameters
.InstrumentationFieldParameters;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
188
Instrumentation
.SimpleInstrumentationContext.noOp;
@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"
);
}
};
189
Instrumentation
}
}
We declare a new class FieldCountState holding the state we are interested in. The
createState method creates a new instance per execution. In the beginField hook,
we simply increment the counter. After the execution finishes, we log the overall field
counter.
This instrumentation is automatically detected and registered by Spring for GraphQL as
it is annotated with the @Component annotation.
ChainedInstrumentation
Spring for GraphQL automatically chains all detected instrumentation beans. No further
configuration is required.
If using pure GraphQL Java, the instrumentations must be manually chained together
via a ChainedInstrumentation. The Instrumentation objects are called in the order
they are defined in.
In GraphQL Java, multiple instrumentations are manually chained together with
ChainedInstrumentation and then this is passed to the graphql.GraphQL builder.
ChainedInstrumentation chainedInstrumentation
= new ChainedInstrumentation(chainedList);
Built-in instrumentations
190
Instrumentation
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 complex operations.
Instrumentation
MaxQueryDepthInstrumentation Prevents execution of very large operations.
TracingInstrumentation Implements the Apollo Tracing1 format.
You can fully customise your instrumentation to hook into steps of GraphQL execution.
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 document and/or the
AndVariables variables
instrumentSchema Allows for changing the GraphQLSchema
instrumentExecutionContext Allows for changing the ExecutionContext class that is
used by GraphQL Java internally during execution.
instrumentDataFetcher Allows for changing a DataFetcher right before it is
invoked
instrumentExecutionResult Allows for changing the overall execution result
1
https://github.com/apollographql/apollo-tracing
191
Instrumentation
192
DataLoader
In this chapter, we will discuss DataLoader, the library used by GraphQL Java to batch
and cache requests for data. We will discuss the common n+1 problem and how to solve
it with DataLoader’s batching feature. We will also discuss how DataLoader’s caching
feature makes data requests more efficient.
We will demonstrate how to use DataLoader in Spring for GraphQL. Then we’ll take a
closer look at how DataLoader works in GraphQL Java.
The n+1 problem is when related entities of an object are retrieved inefficiently, which
can cause significant performance problems. It is a common problem when implementing
DataFetchers for a schema. Note that this is a general problem occurring in other contexts
such as SQL, it is not specific to GraphQL Java nor GraphQL.
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
}
query importantPeople {
people {
bestFriend {
name
}
}
}
193
DataLoader
Let’s register two DataFetchers responsible for loading people and then their bestFriend
in Spring for GraphQL.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@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.
The solution is instead of making one service call for each person, we load all the best
friends of all the people at once. The loading of best friends is deferred, so they can
194
DataLoader
be loaded together. This would reduce the number of service calls from n+1 to two,
regardless of the number of people.
The n+1 problem is so common that the solution is built into GraphQL Java, and can
be accessed in Spring for GraphQL with the controller annotation @BatchMapping. The
solution makes use of the library java-dataloader1 , which is maintained by the GraphQL
Java team. This library is a port of the JS library DataLoader2 . Note that in this book,
we will call the Java library “DataLoader” for short, and make it explicitly clear when
we talk about the JS DataLoader.
Implementing this solution is a small change in Spring for GraphQL.
package myservice.service;
import org.springframework.graphql.data.method.annotation.BatchMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
import java.util.stream.Collectors;
@Controller
record PersonController(PersonService personService) {
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
195
DataLoader
bestFriend and the type name defaults to the simple class name of the input List
element type, Person.
Change the bestFriend method argument to a list of people, and add logic to collect
IDs from the list of people. For the DataLoader to work, the PersonService must offer
a bulk retrieval method getPeopleById.
The @BatchMapping annotated method takes a list of people, then loads all their best
friends at once. Only two service calls are made, instead of n+1. There is quite a bit of
Spring automated magic happening here, which we will explain in greater detail in this
chapter.
DataLoader overview
DataLoader is a library used by GraphQL Java to batch and cache data requests. Batching
solves the n+1 problem, and caching makes data requests more efficient. This library is
also maintained by the GraphQL Java team.
There are two key implementation concepts. A DataLoader instance is conceptually a
layer deferring the loading of entities, which identified by some key. The loading is deferred
until triggered by a “dispatch”. The timing of the dispatch is managed by GraphQL Java.
Then all the data is loaded as a batch, with the logic in a user-implemented BatchLoader.
Note that the library is called “DataLoader”, and one of the key classes is also called
“DataLoader”. To distinguish between the two, we refer to the class with the words
“class” or “instance”.
It’s interesting to note that DataLoader is not specific to GraphQL and is not part of
the GraphQL specification. The two core features, batching and caching, can be applied
generally.
To better understand how DataLoader works, we will walk through multiple examples.
As DataLoader is not specific to GraphQL, let’s start with a simple example without any
GraphQL concepts.
// Setup
UserService userService = ...;
// expected to return a CompletableFuture
BatchLoader<Integer, User> userBatchLoader = userIds ->
userService.loadUsersById(userIds);
// Usage
CompletableFuture<User> user1CF = userLoader.load(1);
196
DataLoader
Let’s walk through a DataLoader example with GraphQL concepts. We will show
this example with pure GraphQL Java as a way to explain DataLoader without the
197
DataLoader
Spring automated magic. You don’t need to write this code as Spring for GraphQL’s
@BatchMapping controller annotation eliminates much of this boilerplate code.
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;
import java.util.List;
import java.util.concurrent.CompletableFuture;
type Person {
name: String
bestFriend: Person
}
198
DataLoader
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse(sdl);
// Per request:
199
DataLoader
// Execute query
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
ExecutionResult executionResult = graphQL.execute(executionInput);
}
}
200
DataLoader
Now that we’ve seen the pure GraphQL Java way to use DataLoader, we’ll walk through
how to implement the same logic in Spring for GraphQL. We’ll begin with an example that
does not use the @BatchMapping controller annotation to demonstrate how it works.
In Spring for GraphQL, a single, central BatchLoaderRegistry exposes factory methods
and a builder to create and register BatchLoaders. The Spring Boot starter declares a
BatchLoaderRegistry bean, which can be injected into a component such as a controller
in the example below. Spring for GraphQL injects the DataLoader instance into the
DataFetcher.
package myservice.service;
import org.dataloader.DataLoader;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Controller
class PersonController {
PersonService personService;
PersonController(
PersonService personService,
BatchLoaderRegistry batchLoaderRegistry) {
this.personService = personService;
201
DataLoader
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
package myservice.service;
import org.springframework.graphql.data.method.annotation.BatchMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
import java.util.stream.Collectors;
@Controller
class PersonController {
PersonService personService;
202
DataLoader
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
203
DataLoader
Batch mapping methods support the following arguments in Spring for GraphQL:
Argument Description
List<T> The list of source objects
java.security.Principal Spring Security principal
@ContextValue(name = A specific value from the GraphQLContext
“foo”)
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.
204
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.
In this chapter, we will make use of standard testing libraries. We will use JUnit 51 ,
Mockito2 , AssertJ3 , and the standard Spring Boot testing capabilities.
DataFetchers are the central concept for implementing a Spring for GraphQL service,
because they are the link between your schema and the data. The main guideline for
writing DataFetchers is that they should be very thin and delegate the actual work to
a layer of code below it. The only purpose of a DataFetcher should be to take care of
GraphQL-specific aspects. Some examples are reading and validation input, mapping
return data, handling exceptions, and converting it to an error.
By purposefully designing DataFetchers to be thin, they also become easier to write unit
tests for.
In the first DataFetchers chapter, we discussed that DataFetchers in Spring for GraphQL
are registered via controller methods with a @SchemaMapping annotation, or one of the
shortcut annotations such as @QueryMapping.
Initially, we’ll explore testing with a simple Hello World example, to illustrate the different
types of testing with Spring for GraphQL. Later in this chapter, we’ll also write tests for
a larger Pet schema.
Here is the simple schema for our Hello World example:
type Query {
hello: String
}
1
https://junit.org/junit5/docs/current/user-guide/
2
https://javadoc.io/static/org.mockito/mockito-core/4.5.1/org/mockito/Mockito.html
3
https://assertj.github.io/doc/
205
Testing
A simple query:
query greeting {
hello
}
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class GreetingController {
@QueryMapping
String hello() {
return "Hello, world!";
}
}
The hello DataFetcher is a good example of a thin DataFetcher that can be easily unit
tested.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class GreetingControllerTest {
@Test
void testHelloDataFetcher() {
GreetingController greetingController = new GreetingController();
206
Testing
}
}
If it becomes difficult to unit test a DataFetcher, we should try to break it apart and
write multiple smaller unit tests.
As we saw in the DataFetchers chapter, a DataFetcher’s get method accepts an input
DataFetchingEnvironment, which is a Java interface containing the necessary GraphQL
information to fetch data, including source, schema, document, context, selection set, and
much more. We also saw in the DataFetchers chapter that Spring for GraphQL provides
shortcuts to particular fields in the DataFetchingEnvironment, to be used as inputs
into DataFetchers registered via controller annotations. When writing DataFetcher tests,
we recommend mocking the DataFetchingEnvironment and only implement the parts
needed.
GraphQlTester
These different classes are normally only used to create a GraphQlTester instance. For
use in code, we only reference GraphQlTester.
For example, here is a query for our Hello World example from earlier in this chapter:
query greeting {
hello
}
With GraphQlTester, we can write a test to verify this query is executed, and
returns the expected response “Hello, world!”. This example demonstrates the
HttpGraphQlTester.
207
Testing
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
@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.
Let’s walk through each of these method calls in this test.
document or documentName
208
Testing
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!");
interface Traversable {
Path path(String path);
}
We can use any JsonPath4 with path. In our Hello World example, we used the path
"hello". In the more complex Pets example later in this chapter, we’ll see how to select
names from a list of Pets.
4
https://github.com/json-path/JsonPath
209
Testing
type Query {
favoritePet: Pet
}
type Pet {
name: String
}
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class PetsController {
@QueryMapping
Pet favoritePet() {
// return favorite pet from database
}
To convert the current part of the GraphQL response into a Pet entity in GraphQlTester,
use entity(Pet.class), then test your assertion afterwards:
210
Testing
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.
However, in a test, you usually want to check that no errors were returned. To verify
that no errors are returned in a test, add .errors().verify().
graphQlTester
.document("query greeting { hello }")
.execute()
.errors()
.verify() // Ensure there are no GraphQL errors
.path("hello")
.entity(String.class)
.isEqualTo("Hello, world!");
As explained in the Spring for GraphQL overview at the beginning of the book, there
are three key Spring for GraphQL classes: GraphQlHttpHandler, WebGraphQlHandler,
and ExecutionGraphQlService. We can also test the HTTP transport layer.
To recap, a request passes through three primary classes in Spring for GraphQL, each
with a distinct responsibility, as shown in Figure 1:
211
Testing
• End-to-end over HTTP: creating an entirely separate process and test via HTTP
• Application test: test in the same process without the full HTTP layer, includes a
client
• Test on server side (without a client) starting from WebGraphQLHandler
• Test on server side (without a client) starting from ExecutionGraphQlService
Note that the next few sections focus on HTTP where tests include transport. For
WebSocket tests, see subscriptions testing section later in this chapter.
Spring Boot allows us to start a whole service as a separate process and test it end-to-end
over HTTP by using the SpringBootTest annotation.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
212
Testing
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
@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).
To automatically configure a HttpGraphQlTester, use the @AutoConfigureHttpGraphQlTester
annotation.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester
.AutoConfigureHttpGraphQlTester;
213
Testing
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureHttpGraphQlTester
class MockedTest {
@Autowired
HttpGraphQlTester graphQlTester;
@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
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.test.tester.WebGraphQlTester;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class WebGraphQlTest {
214
Testing
@Autowired
WebGraphQlHandler webGraphQlHandler;
@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
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.test.tester
.ExecutionGraphQlServiceTester;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class GraphQlServiceTest {
@Autowired
ExecutionGraphQlService graphQlService;
@Test
void testHello() {
ExecutionGraphQlServiceTester graphQlServiceTester
= ExecutionGraphQlServiceTester.create(graphQlService);
String document = "query greeting { hello }";
215
Testing
graphQlServiceTester.document(document)
.execute()
.path("hello")
.entity(String.class)
.isEqualTo("Hello, world!");
}
For a more minimal testing setup, we can use the @GraphQlTest annotation in-
stead of @SpringBootTest. @GraphQlTest configures a slice test, which will load
only a subset of an application, focusing only on the GraphQL layer. It is a
ExecutionGraphQlServiceTester, with the added feature of only automatically loading
what is strictly needed to execute the request, and nothing else.
Let’s examine @GraphQlTest with a Pet service. This is the schema:
type Query {
pets: [Pet]
}
type Pet {
name: String
}
query myPets {
pets {
name
}
}
This is the Pet controller, which includes a static Pet class and the pets DataFetcher
annotated with @QueryMapping:
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
216
Testing
import org.springframework.stereotype.Controller;
import java.util.List;
@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;
import org.springframework.stereotype.Service;
@Service
class PetService {
List<Pet> getPets() {
// Fetch data from database, or elsewhere
}
In this example, our pets DataFetcher is very thin, it only delegates to PetService.
This is quite realistic, and usually we aim for this kind of design.
Now with @GraphQlTest, our test setup includes the PetController, but not the
PetService because it doesn’t belong to the GraphQL layer itself.
This means we need to create a mock for the PetService.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
217
Testing
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.graphql.test.tester.GraphQlTester;
import java.util.List;
@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"));
}
This is a suitable setup, especially if we have a complicated setup below the controller.
Mocking the Pet service dependency allows for a very focused and lean test.
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);
218
Testing
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 JsonPath5 with
path.
Subscription testing
type Subscription {
hello: String
}
package myservice.service;
import org.springframework.graphql.data.method.annotation
.SubscriptionMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
@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);
}
}
5
https://github.com/json-path/JsonPath
219
Testing
spring.graphql.websocket.path=/graphql
package myservice.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebSocketGraphQlTester;
import org.springframework.web.reactive.socket.client
.ReactorNettyWebSocketClient;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.net.URI;
@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);
220
Testing
StepVerifier.create(hello)
.expectNext("Hello 0")
.expectNext("Hello 1")
.expectNext("Hello 2")
.verifyComplete();
}
}
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.
Here are some examples:
• If we want to test our DataFetcher calling the PetService, a unit test is the right
approach.
• If we want to make sure our GraphQL setup is correct (e.g. verify that a DataFetcher
is mapped to the correct field), @GraphQlTest is the right approach.
• If we want to test a WebGraphQlInterceptor, then a WebGraphQlHandler test is
most often good enough.
It is good to have some basic end-to-end Spring Boot tests, ensuring that the whole service
starts and receives requests as expected. However, keep in mind that these end-to-end
tests are mostly focused on setup. If you want to focus on verifying behavior, it is better
to use a mock environment.
Testing all layers together involves running a SpringBootTest in a mock environment
with a HttpGraphQlHandler. This is useful to test security which we will discuss in the
next chapter on Security.
These testing guidelines fit into the Test Pyramid model6 . The idea is to have more of fo-
cused, smaller and faster tests, compared to the number of tests that run longer, test more
6
https://martinfowler.com/bliki/TestPyramid.html
221
Testing
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.
In this chapter, we covered unit testing, application testing, and end-to-end testing with
Spring for GraphQL. We also discussed testing best practices and how to think about
testing your Spring for GraphQL service.
222
Security
Spring for GraphQL makes securing GraphQL much simpler by integrating familiar
concepts from Spring Security1 .
In this chapter, auth is short for both authentication and authorization.
Although securing GraphQL services is important, note that the GraphQL specification
does not prescribe any specific auth logic, because it does not dictate where the data
comes from. As a result of this, GraphQL Java does not provide auth support. Another
key reason for omitting auth support in GraphQL Java was the impracticality of a
generic engine attempting to support auth with is tightly coupled with transport layer
concerns.
As security is not part of the GraphQL Java engine, this chapter will instead focus on
using Spring for GraphQL and Spring Security2 to secure your GraphQL service. Spring
for GraphQL has built-in, dedicated support for Spring Security.
We’ll walk through key concepts with an example service for managing store orders. We
intend for the important parts to be realistic to ensure we have a working and secured
service, while we will take some shortcuts to keep this chapter short.
A web client will use our online store with a GraphQL API via HTTP, where they can
query all the orders for the current user. For brevity, only admins can remove an order,
and we will not cover order creation.
Here is our store orders schema.
type Query {
# Every logged-in user can query orders
myOrders: [Order]
}
type Order {
1
https://spring.io/projects/spring-security
2
https://spring.io/projects/spring-security
223
Security
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;
package myservice.service;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@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"),
224
Security
This simple Java class includes the basic functional aspects we want: getOrdersByOwner
returns the list of orders for the provided owner and deleteOrder deletes an order.
Our authentication will use session cookies and we require a valid session for every request.
This means that before the execution of a request starts, we need to ensure that we have
a valid session, otherwise we return an HTTP 401 status code.
Once we have a valid session identifying the user, we can query their orders and check if
they can delete an order. This authorization part is handled during GraphQL execution
on the DataFetcher-level.
To add Spring Security to our project, add the dependency
org.springframework.boot:spring-boot-starter-security
and then create the following Config class.
package myservice.service;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive
.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
225
Security
import org.springframework.security.core.userdetails
.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication
.RedirectServerAuthenticationSuccessHandler;
@Configuration
@EnableWebFluxSecurity
class Config {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http)
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.withDefaultPasswordEncoder();
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);
}
}
• It activates Spring Security for reactive WebFlux applications with the annotation
226
Security
@EnableWebFluxSecurity.
• It configures a simple form-based login, which should redirect to /graphiql if
successful. GraphiQL3 is an interactive playground which is included in Spring for
GraphQL.
• It disables CSRF4 protection to allow us to use GraphiQL for easy testing. We do
not recommend disabling CSRF for a real service.
• It requires that every request is authenticated.
• It configures the list of users, simply by declaring them and including their password
directly in our configuration. Of course, hard coded users is only acceptable for
demo code.
The simple form-based login allows us to login by visiting /login in a browser and to
logout via /logout. If not logged in, we are get redirected to /login. This is a very
convenient way for us to switch users easily and verify the expected behavior manually.
Let’s create the last required class, the OrderController that implements our
DataFetcher.
To implement the myOrders method, we need to know the current logged-in user. Spring
for GraphQL provides access to the current java.security.Principal by simply declar-
ing it as a Java argument. Spring for GraphQL made this possible by integrating with
Spring Security.
package myservice.service;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import java.security.Principal;
import java.util.List;
@Controller
record OrderController(OrderService orderService) {
@QueryMapping
List<Order> myOrders(Principal principal) {
return orderService.getOrdersByOwner(principal.getName());
}
This is the first part of implementing authorization. We use the current user to filter the
list of orders returned. We only return the orders belonging to the right user.
3
https://github.com/graphql/graphiql
4
https://en.wikipedia.org/wiki/Cross-site_request_forgery
227
Security
The second DataFetcher is concerned with deleting an order. While we already made
sure that we have valid authentication, we must also check authorization. Only admins
can delete orders. Therefore, we need to verify the role of the current user before we
delete any orders.
package myservice.service;
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation.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;
import java.security.Principal;
import java.util.List;
@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;
228
Security
if (!user.getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
throw new AccessDeniedException("Only admins can delete orders");
}
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.
We can test this service manually by logging in as “Luna” at /login with the password
“password” and querying all orders via the GraphiQL playground, as shown in Figure
1.
query lunaOrders {
myOrders {
id
}
}
If we try to delete an order as Luna, we will get a GraphQL error telling us we are
unauthorized, as shown in Figure 2.
mutation lunaUnauthorized {
deleteOrder(input: {orderId: 1}) {
success
}
}
After we log out via /logout and login as “Andi” (with password “password”), we can
delete an order.
It’s important to note that an unauthorized attempt to delete an order with a logged-in
user, will not cause an HTTP error. The status code of the HTTP response is 200
containing a GraphQL error.
229
Security
230
Security
In the store orders example, we saw one feature of Spring for GraphQL supporting Spring
Security: we can declare a java.security.Principal as input for annotated methods
(this includes @BatchMapping).
Spring for GraphQL also automatically registers a ReactiveSecurityDataFetcherExceptionResolver
(or SecurityDataFetcherExceptionResolver for WebMVC) handling AuthenticationException
and AccessDeniedException. AuthenticationException and AccessDeniedException
result in a GraphQL error with error type UNAUTHORIZED and FORBIDDEN respectively.
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.
231
Security
@PreAuthorize("hasRole('ADMIN')")
Mono<Boolean> deleteOrder(String orderId) {
return Mono.just(orders.removeIf(order -> order.id().equals(orderId)));
}
In order for PreAuthorize to work in a WebFlux service, the annotated method must
return Mono or Flux. Therefore, we need to wrap our return value in a Mono.
We also refactor getOrdersByOwner into getOrdersForCurrentUser and we are using
ReactiveSecurityContextHolder to get the current logged-in user.
Mono<List<Order>> getOrdersForCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> {
Principal principal = securityContext.getAuthentication();
return orders
.stream()
.filter(order -> order.owner().equals(principal.getName()))
.collect(Collectors.toList());
});
}
package myservice.service;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method
.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive
.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails
.MapReactiveUserDetailsService;
232
Security
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication
.RedirectServerAuthenticationSuccessHandler;
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class Config {
@Bean
SecurityWebFilterChain springWebFilterChain(
ServerHttpSecurity http
) 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.withDefaultPasswordEncoder();
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);
}
}
233
Security
package myservice.service;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation
.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import reactor.core.publisher.Mono;
import java.util.List;
@Controller
record OrderController(OrderService orderService) {
@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);
}
package myservice.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context
.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
234
Security
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")
));
}
Mono<List<Order>> getOrdersForCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> {
Principal principal = securityContext.getAuthentication();
return orders
.stream()
.filter(order -> order.owner().equals(principal.getName()))
.collect(Collectors.toList());
});
}
@PreAuthorize("hasRole('ADMIN')")
Mono<Boolean> deleteOrder(String orderId) {
return Mono.just(orders
.removeIf(order -> order.id().equals(orderId)));
}
235
Security
Testing auth
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive
.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.util.Map;
@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);
}
}
236
Security
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;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlResponse;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.security.authentication
.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority
.SimpleGrantedAuthority;
import org.springframework.security.core.context
.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.List;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Import(WebGraphQlTest.WebInterceptor.class)
class WebGraphQlTest {
@Autowired
WebGraphQlHandler webGraphQlHandler;
@Component
static class WebInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request,
237
Security
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
}
}
We register a new WebInterceptor and create the authenticated user because Spring
Security relies on the SecurityContext, which itself is being stored in the Reactor
context. In the test itself, we verify that we return no error and then check the order
IDs in the response. These order IDs were previously defined in the OrderService
constructor.
Within the same class, we can also test that an unauthorized user cannot delete orders.
@Test
void testMutationForbidden() {
238
Security
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.
In this chapter, we covered how to secure GraphQL services with Spring for GraphQL’s
useful Spring Security integrations.
239
Java client
Spring for GraphQL comes with a client, GraphQlClient, for making GraphQL requests
over HTTP or WebSocket.
The HttpGraphQlClient is for queries and mutations, to execute GraphQL requests
over HTTP. The WebSocketGraphQlClient is for subscriptions, and executes GraphQL
requests over a shared WebSocket connection.
We interact with the base Java interface GraphQlClient. We use HttpGraphQlClient
and WebSocketGraphQlClient to create specific instances implementing the
GraphQlClient interface. The design is similar to GraphQlTester, which we
discussed previously in the Testing chapter.
HTTP client
package myservice.service;
import org.springframework.graphql.client.HttpGraphQlClient;
import org.springframework.web.reactive.function.client.WebClient;
Using builder methods, we can provide specific HTTP settings, such as headers at build
time.
or
240
Java client
Once created, we can’t change any of these client settings. For different settings, we need
to mutate the client and use the builder methods again.
WebSocket client
import org.springframework.graphql.client.WebSocketGraphQlClient;
import org.springframework.web.reactive.socket.client
.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
1
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-
websocket-client
241
Java client
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.
Once created, we can’t change any WebSocket client settings. For different settings, we
need to mutate the client and use the builder methods again.
GraphQlClient
242
Java client
In this chapter, we covered Spring for GraphQL’s built-in GraphQlClient, for making
GraphQL requests over HTTP or WebSocket.
243