Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
30 views

Graphql Java

Uploaded by

pawga777
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
30 views

Graphql Java

Uploaded by

pawga777
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 252

Contents

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

Documentation with descriptions . . . . . . . . . . . . . . . . . . . . . . . . . . 31


Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

GraphQL query language 33


Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Query operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Mutation operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Subscription operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Fragments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Inline fragments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Aliases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
GraphQL document . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Named and unnamed operations . . . . . . . . . . . . . . . . . . . . . . . 44
Query language in GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . 46

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

Building a GraphQL service 71


Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
GraphQL Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Spring WebFlux or Spring MVC . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Reading schemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Configuration properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Expanding our Spring for GraphQL service . . . . . . . . . . . . . . . . . . . . 73
Pet schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Fetching data from an external service . . . . . . . . . . . . . . . . . . . . . . . 75
Source object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

iii
Contents

GraphQL arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Unions, interfaces, and TypeResolver . . . . . . . . . . . . . . . . . . . . . . . . 86

Subscriptions 91
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
Client support . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

Request and response 98


Transport protocols and serialization . . . . . . . . . . . . . . . . . . . . . . . . 98
Request . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
Response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
HTTP status codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
HTTP headers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
Intercepting requests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

GraphQL errors 106


Request errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Field errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
How errors appear in the response . . . . . . . . . . . . . . . . . . . . . . . . . 111
Error classifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
How to return errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Throw exception during DataFetcher invocation . . . . . . . . . . . . . . . . . . 114
Customizing exception resolution . . . . . . . . . . . . . . . . . . . . . . . 115
Return data and errors with DataFetcherResult . . . . . . . . . . . . . . . . . 116

Schema design 118


Schema-first and implementation-agnostic . . . . . . . . . . . . . . . . . . . . . 118
Evolution over versioning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Connected . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Schema elements are cheap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Nullable fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
Nullable input fields and arguments . . . . . . . . . . . . . . . . . . . . . . . . 129
Pagination for lists with Relay’s cursor connection specification . . . . . . . . . 130
Relay’s cursor connections specification . . . . . . . . . . . . . . . . . . . 131
Schema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Query and response . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Requesting more pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
Key concepts of Relay’s cursor connections specification . . . . . . . . . . 134
Expected errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
Mutation format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Naming standards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

iv
Contents

DataFetchers in depth 142


More DataFetcher inputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Global context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Local context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
DataFetcher implementation patterns . . . . . . . . . . . . . . . . . . . . . . . 148
Spring for GraphQL Reactor support . . . . . . . . . . . . . . . . . . . . . . . . 151

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

Java client 240


HTTP client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
WebSocket client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
GraphQlClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

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.

About this book

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

Your first Spring for GraphQL service

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

Figure 1: Spring Start

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;

record Pet(String name, String color) {


}

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

Figure 3: GraphiQL Documentation Explorer

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.

Figure 4: GraphiQL Pet documentation

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:

• Created a new GraphQL service with WebFlux via https://start.spring.io


• Added a GraphQL schema file in SDL format
• Implemented a GraphQL controller that returns a list of Pet data
• Executed a GraphQL query

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

Figure 5: Client Server

For example, a request to query a list of pets and their names:

# This is a comment

# We are asking for two fields:


# "pets" and the "name" for each pet

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
}
}

the response would look like this:

{
"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 is the root Object, because it is named Query


type Query {
# a field named "pets" which returns a list of Pet
pets: [Pet]
}

# an Object named Pet


type Pet {
# a field with the name "name" of type String
name: String
color: String
}

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

For our API from above, it would return:

{
"__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.

GraphQL is an API technology

We want to highlight one very important aspect of GraphQL: it is an API technology


that is agnostic to the source of your data. When implementing a GraphQL service, the
actual data can come from anywhere. GraphQL is not a database or persistence-specific
technology. And while there are technologies and services that let us expose our database
schema as GraphQL API, that is not a robust architecture when our system will grow:
these are two things that we should not couple together. We highly recommend you
think about a database schema as something different from a GraphQL schema.

A brief history of GraphQL

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:

The GraphQL Foundation is a neutral foundation founded by global technology


and application development companies. The GraphQL Foundation encour-
ages contributions, stewardship, and a shared investment from a broad group
in vendor-neutral events, documentation, tools, and support for GraphQL.

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 .

From GraphQL Java to Spring for GraphQL

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:

• GraphQL Specification (Spec): defines in a technology-independent way what


GraphQL is and what a GraphQL service needs to implement.
• GraphQL Java: implements the GraphQL spec for Java.
• Spring for GraphQL: integrates GraphQL Java with Spring.

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

Figure 1: Three layers

15
Overview

Schema and SDL

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.

GraphQL query language

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.

Request and Response

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

Execution and DataFetcher

We can roughly divide the actual execution of a GraphQL request via HTTP like this:

1. First, the request is deserialized and processed (Spring).


2. We invoke GraphQL Java, which involves fetching data (GraphQL Java).
3. Then we create and serialize the response (Spring).

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

How concepts relate to each other

Figure 2: Concept relations

As shown in Figure 2,

• The SDL defines the schema


• A request contains text in query language format
• The execution of a request uses the schema and DataFetchers to produce a response

GraphQL Java

The primary classes of GraphQL Java are, as shown in Figure 3:


graphql.GraphQL: Normally only one instance per service. Used to start GraphQL Java
execution.
graphql.ExecutionInput: A GraphQL Java request.
graphql.ExecutionResult: A GraphQL Java response.
graphql.schema.GraphQLSchema: Description of the GraphQL API plus the logic needed
to execute it.

19
Overview

graphql.schema.DataFetcher: Java interface to be implemented by every service. Your


GraphQL service will implement DataFetchers via Spring for GraphQL’s schema mapping
annotations.

Figure 3: GraphQL Java classes

Spring for GraphQL

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:

1. A general purpose HTTP request invokes GraphQlHttpHandler converts the request


into a WebGraphQlRequest.
2. WebGraphQlHandler takes the WebGraphQlRequest, and calls ExecutionGraphQlService
to execute the request.
3. ExecutionGraphQlService ultimately invokes GraphQL Java.

Figure 4: Spring for GraphQL classes

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.

Loading schema resources in Spring for GraphQL

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 schema elements

A schema in GraphQL Java is represented as a GraphQLSchema instance, which is a


graph of GraphQLSchemaElements. We will describe the most important elements of a
GraphQLSchema and the notation in SDL format.

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

Figure 1: Input and Output types

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
}

In GraphQL Java, an enum is represented by an instance of GraphQLEnumType.

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

In GraphQL Java, an object type is represented by an instance of GraphQLObjectType


with GraphQLFieldDefinition defining its fields.

Input object

An input object type describes a group of input fields where each has an input type. An
input object name must be unique across the schema.
In SDL, an input object is declared via input.

input PetFilter {
minAge: Int
maxAge: Int
}

type Pet {
name: String
age: Int
}

type Query {
pets(filter: PetFilter): [Pets]
}

Interface

Similar to an object, an interface describes a certain shape of data as a list of fields


and has a name. In contrast to an object, an interface can be implemented by another
interface or object. An interface or object implementing an interface must contain at least
the same fields defined in the interface. In that sense, an interface is used to describe an
abstract shape, which can be realized by different objects.
Similar to objects, every field has an optional list of arguments. Every argument has a
name and type, which must be an input type.
An interface in SDL is declared as interface. Implementations by other interfaces or
objects specify implements followed by the interface name. In the example below, both
Dog and Cat types implements the Pet interface. Both Dog and Cat types contain at
least fields for name and owners, which are declared in the Pet interface.

27
Schema

interface Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
}

# One implementation of the interface


type Dog implements Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
doesBark: Boolean # additional field specific to Dog
}

# 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 |.

union Pet = Dog | Cat

type Dog {
name: String
doesBark: Boolean
}

type Cat {

28
Schema

name: String
doesMeow: Boolean
}

In GraphQL Java, a union is represented as an instance of GraphQLUnionType.

List and NonNull

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

A directive is a schema element that allows us to define metadata for a schema or a


GraphQL operation. A directive needs to declare all possible locations where it can be
used. A directive contains an optional list of arguments, similarly to fields.
An instance of a directive is called an applied directive. We’ll discuss applied directives
in more detail in the Directives chapter.
To declare a directive in SDL:

29
Schema

# This is a directive without any argument


# and it can only used in two locations inside the schema
directive @example on FIELD_DEFINITION | ARGUMENT_DEFINITION

# Example usage
type SomeType {
field(arg: Int @example): String @example
}

In GraphQL Java, a directive declared in the schema is represented as GraphQLDirective.


There’s much more to directives. We will dive into greater detail in the Directives
chapter.

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]
}

Arguments can be optional or required. An argument is required if the argument type is


non-null and does not have a default value. Otherwise, the argument is optional. For
example, the argument name: String! is required.

30
Schema

Documentation with descriptions

Documentation is a powerful feature of GraphQL. Descriptions of GraphQL definitions


such as fields and types are embedded in the schema, alongside their definitions. These
descriptions are made available via introspection, and can be used to generate interactive
documentation.
Nearly every SDL element can have a description in Markdown2 format as documentation.
The description is either a string literal enclosed in quotation marks ", or a block string
wrapped in triple-quotes """ for multi-line documentation with line breaks or additional
Markdown.

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

Figure 2: Documentation in GraphiQL

type Query {
# This is a comment
hello: String
}

# Multi line comment


# requires
# multiple hash signs

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.

• String literals written as "String literal", enclosed with double quotation


marks
• Boolean literals which are true or false
• Int literals written as 123
• Float literals written as 123.45
• A literal with no value (Null) written as null
• List literals written as ["one list element", "another list element"],
wrapped with square brackets
• Enum values written as ENUM_VALUE
• Input object literals written as { someField: "value", anotherOne: {
fieldInField: 123 } }. They represent an unordered list of key input, wrapped
in curly braces

Operations

There are three operations in GraphQL:

• query, a read-only fetch


• mutation, a write followed by a fetch
• subscription, a long-lived request that fetches data in response to events

33
GraphQL query language

In a GraphQL schema, these operations are modelled as root operation types.


In GraphQL query language, these operations also form the root of the query.

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
}
}

Argument types in operations must correspond to their respective schema definitions. If


the schema requires an Int argument value, and we provide a Boolean in a query, the
incorrect argument will cause a validation error.

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:

fragment <FragmentName> on <TypeCondition> {


<fields>
}

Use fragments inside operations with the fragment spread syntax:

... <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.

fragment personDetails on Person {


firstName
lastName
title
}

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>
}

Consider this schema:

type Query {
pets: [Pet]
}

interface Pet {
name: String
}

type Dog implements Pet {


name: String
doesBark: Boolean
}

type Cat implements Pet {


name: String
doesMeow: Boolean
}

We can write inline fragments to query the doesBark field for Dog results and doesMeow
for Cat results.

query allThePets {
pets {
... on Dog {
doesBark
}
... on Cat {
doesMeow
}
}
}

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:

union Food = Pizza | IceCream

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

We can include parameter variables in a GraphQL operation, which enables clients to


reuse operations without needing to dynamically rebuild them.
An operation can declare variables that serve as input for the entire operation.
To declare variables, define them after the operation name. The variable name begins with
$, and is followed by the type. Variables can be marked as non-null with an exclamation
mark !, similarly to field or directive arguments.
For example, the variable petId is of type ID, and it is non-nullable.

query findPet($petId: ID!) {


pet(id: $petId) {
name
}
}

A default value can be provided, defined after an equals sign =.

query findPet2($petId: ID! = "12345DefaultId") {


pet(id: $petId) {
name
}
}

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.

mutation changePetName($petId: ID!, $petName: String) {


changePetName(id: $petId, name: $petName) {
success
}
}

Variable values are sent alongside the operation. For example, we want to find a pet with
ID 9000.

query findPet($petId: ID!) {


pet(id: $petId) {
name
}
}

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")
}

This query will produce a response including the data:

{
"search1": "Foo result",
"search2": "Bar result"
}

GraphQL document

A GraphQL executable document (often shortened to document) is a text written


in GraphQL query language notation that contains at least one query, mutation, or
subscription operation and an optional list of fragments.

Named and unnamed operations

The operation name is the word following the query, mutation, or subscription keyword.
In the following example, the operation name is findPet.

query findPet($petId: ID!) {


pet(id: $petId) {
name
}
}

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
}

fragment helloFragment on Query {


hello
}

45
GraphQL query language

Query language in GraphQL Java

In GraphQL Java, a query language text is parsed and transformed into an


abstract syntax tree (AST) of graphql.language.Node instances. For ex-
ample, a field is a graphql.language.Field and an inline fragment is a
graphql.language.InlineFragment.
GraphQL Java takes care of parsing and validating the request, including the query
language text, so normally we do not require direct access to Node instances. We won’t go
into further depth on this topic as it’s an engine detail and not relevant for implementing
a GraphQL service.
In this chapter, we covered GraphQL query language and the three operations, query,
mutation, and subscriptions. In the next chapter we’ll discuss DataFetchers, the methods
which populate data for fields in a schema.

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

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

Spring for GraphQL annotated methods

Spring for GraphQL provides an annotation-based programming model, where annotated


methods are registered as DataFetchers. In the second half of this chapter, we’ll take a
closer look at the DataFetcher interface inside the GraphQL Java engine. For now, we’ll
see how Spring for GraphQL controller annotations achieve exactly the same result with
far less boilerplate code.
Before we go on, there are a few key classes which work closely with DataFetchers. In
GraphQL Java, a GraphQLSchema is both the structure (or shape) of the API, and all
the logic needed to execute requests against it. Another key GraphQL Java concept
is RuntimeWiring, which contains GraphQLCodeRegistry, a map of schema fields to
DataFetchers. Each DataFetcher needs to be registered in the GraphQLCodeRegistry
inside RuntimeWiring.
Annotated controller methods enable us to implement a DataFetcher and also automati-
cally register into the GraphQLCodeRegistry inside RuntimeWiring. While the Spring
Boot starter automatically initializes and manages RuntimeWiring for GraphQL Java,
you also have the full flexibility to manually access and modify it. Later in this chapter,
we’ll see how to modify RuntimeWiring when we demonstrate how to add a custom
scalar.
There are four different schema mapping annotations available in Spring for GraphQL:
the general @SchemaMapping, and three shortcut annotations, @QueryMapping,
@MutationMapping, and @SubscriptionMapping.
@QueryMapping is a shortcut annotation for the DataFetcher for the query type,
@MutationMapping and @SubscriptionMapping are for mutation and subscription types
respectively. All three are shortcuts for the general @SchemaMapping annotation, which
takes arguments typeName and field, to indicate which type and field the DataFetcher
should be mapped to.
Let’s see these annotations in an example. Here is a Pet schema:

type Query {
favoritePet: Pet
}

type Pet {
name: String
owner: Person
}

type Person {
firstName: String

48
DataFetchers

lastName: String
}

With Pet and Owner record classes:

package myservice.service;

record Pet(String name, String ownerId) {


}

package myservice.service;

record Person(String id, String firstName, String lastName) {


}

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();
}

@SchemaMapping(typeName = "Pet", field = "owner")


Person owner(Pet pet) {
return petService.getPerson(pet.ownerId());
}

The favoritePet method is annotated with @QueryMapping, indicating it will be reg-


istered to a field in the Query type. By default, Spring for GraphQL will determine
the field name from the method name. This means the favoritePet method will be
registered as a DataFetcher for the field favoritePet in the type Query.

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.

PropertyDataFetchers in Spring for GraphQL

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

Figure 1: DataFetcher per field

package myservice.service;

record Pet(String name, String ownerId) {


}

And this is the Person record class:

package myservice.service;

record Person(String id, String firstName, String lastName) {


}

The parent field of Pet.name is Query.favoritePet and our DataFetcher favoritePet


returns the Java object Pet containing the property name exposed via the standard Java
getter convention, so the schema GraphQL field name matches exactly the Java property.
The same is true for Person: the second DataFetcher owner returns a Person Java object
with a structure identical to the schema: both contain two strings named firstName and
lastName.
A PropertyDataFetcher fetches the data from a Java object as long as the

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.

DataFetchers and schema mapping handler methods

A quick note on naming. @SchemaMapping and shortcut annotations @QueryMapping,


@MutationMapping, and @SubscriptionMapping, declare schema mapping handler meth-
ods, which are then registered as DataFetchers by Spring for GraphQL. There is a
one-to-one relationship between a schema mapping handler method and a DataFetcher.
Therefore, in the remainder of this book, we’ll use the word DataFetcher for these schema
mapping handler methods.
Technically speaking, the schema mapping handler methods do not implement the
DataFetcher interface directly. It is the Spring for GraphQL AnnotatedControllerConfigurer
that registers these handler methods as DataFetchers. However, as these concepts
map one-to-one, we want you to think of these schema mapping handler methods as
DataFetchers.

TypeResolver in Spring for GraphQL

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 Dog implements Pet {


name: String
owner: Person
doesBark: Boolean
}

type Cat implements Pet {


name: String
owner: Person
doesMeow: Boolean
}

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;

record Dog(String name, String ownerId, Boolean doesBark)


implements Pet {
}

package myservice.service;

record Cat(String name, String ownerId, Boolean doesMeow)


implements Pet {
}

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

Here’s a sample query for favorite pet:

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;

import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;

@Configuration
class Config {
TypeResolver petTypeResolver = (env) -> {
// Your custom type resolver logic
};

@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {

55
DataFetchers

return wiringBuilder -> {


TypeRuntimeWiring petWiring = newTypeWiring("Pet")
.typeResolver(petTypeResolver)
.build();

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.

Arguments in Spring for GraphQL

To access a GraphQL argument, declare them via the @Argument annotation.


For example, a search field in the schema takes arguments.

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
}

The SearchInput record class:

57
DataFetchers

package myservice.service;

record SearchInput(String pattern, int limit) {


}

And the search controller method:

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

Map<String, Object> input = env.getArgument("input");


String pattern = (String) input.get("pattern");
int limit = (int) input.get("limit");
// Your search logic here
}

More Spring for GraphQL inputs

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

Adding custom scalars in Spring for GraphQL

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);
}

The Spring Boot Starter automatically detects all RuntimeWiringConfigurer beans.

Under the hood: DataFetchers inside GraphQL Java

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.

DataFetchers in GraphQL Java

Recall that a GraphQL operation is basically a tree of fields. We need to instruct


GraphQL Java how to load the data for every field. Therefore, every field must have an
associated DataFetcher.

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.

public interface DataFetcher<T> {


T get(DataFetchingEnvironment environment) throws Exception;
}

The interface has only one method get with one argument DataFetchingEnvironment.
The returned result can be anything. This interface directly reflects a core principle of
GraphQL, it is agnostic about where the data comes from.
Let’s implement DataFetchers for the simple Pet schema in the earlier Spring for GraphQL
example. Note that we are implementing the initial example, where Pet is an object type
and not an interface.

type Query {
favoritePet: Pet
}

type Pet {
name: String
owner: Person
}

type Person {
firstName: String
lastName: String
}

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.

// Lower level layer service does the work of retrieving data


PetService petService = new PetService();

DataFetcher<Pet> favoritePetDataFetcher = (env) -> {


return petService.getFavoritePet();
};

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;

record Pet(String name, String ownerId) {


}

And this is the Person record class:

package myservice.service;

record Person(String id, String firstName, String lastName) {


}

As Pet contains only a ownerId and not a full Person object, we need to load more data.
Let’s implement another DataFetcher.

// Lower level layer service does the work of retrieving data


PetService petService = new PetService();

DataFetcher<Pet> favoritePetDataFetcher = (env) -> {


return petService.getFavoritePet();
};

DataFetcher<Person> ownerDataFetcher = (env) -> {


// Load the correct Person for the Pet
Pet pet = env.getSource();
return petService.getPerson(pet.ownerId());
};

In the ownerDataFetcher, we need to access the source object of the DataFetchingEnvironment,


which is an object of type Pet. The pet instance information is necessary in order to
retrieve the details of the owner.
To highlight the convenience of Spring for GraphQL, let’s compare to the owner
DataFetcher we wrote previously:

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().

Source objects in GraphQL Java

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.

<T> T getSource(); // in DataFetchingEnvironment

In the previous section, we implemented an ownerDataFetcher. The purpose of the


ownerDataFetcher is to load a Person.

DataFetcher<Person> ownerDataFetcher = (env) -> {


// Load the correct Person for the Pet
Pet pet = env.getSource();
return petService.getPerson(pet.ownerId());
};

We know that the parent DataFetcher favoritePet returned a Pet. We can access the
parent DataFetcher result via the source object of the DataFetchingEnvironment.

Pet pet = env.getSource();

Then use the pet’s owner ID to look up the owner:

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.

RuntimeWiring in GraphQL Java

RuntimeWiring is the GraphQL Java class which contains GraphQLCodeRegistry, which


maps schema fields to DataFetchers.
We have implemented our two DataFetchers, but we haven’t yet instructed GraphQL
Java which schema fields these correspond to. This is how to manually register the
DataFetchers in the RuntimeWiring with pure GraphQL Java code.

TypeRuntimeWiring queryWiring = newTypeWiring("Query")


.dataFetcher("favoritePet", favoritePetDataFetcher)
.build();
TypeRuntimeWiring petWiring = newTypeWiring("Pet")
.dataFetcher("owner", ownerDataFetcher)
.build();

RuntimeWiring runtimeWiring = newRuntimeWiring()


.type(queryWiring)
.type(petWiring)
.build();

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.

Creating an executable schema in GraphQL Java

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;

import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring;


import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;

public class PureGraphQLJava {

public static void main(String[] args) {


String sdl = """
type Query {
favoritePet: Pet
}

type Pet {
name: String
owner: Person
}

type Person {
firstName: String
lastName: String
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse(sdl);

// Lower level layer service does the work of retrieving data


PetService petService = new PetService();

66
DataFetchers

DataFetcher<Pet> favoritePetDataFetcher = (env) -> {


return petService.getFavoritePet();
};

DataFetcher<Person> ownerDataFetcher = (env) -> {


// Load the correct Person for the Pet
Pet pet = env.getSource();
return petService.getPerson(pet.ownerId());
};

TypeRuntimeWiring queryWiring = newTypeWiring("Query")


.dataFetcher("favoritePet", favoritePetDataFetcher)
.build();
TypeRuntimeWiring petWiring = newTypeWiring("Pet")
.dataFetcher("owner", ownerDataFetcher)
.build();

RuntimeWiring runtimeWiring = newRuntimeWiring()


.type(queryWiring)
.type(petWiring)
.build();

GraphQLSchema schema = new SchemaGenerator()


.makeExecutableSchema(parsedSdl, runtimeWiring);
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
System.out.println("data:" + graphQL
.execute("query pureGJ {favoritePet{name owner{firstName}}}")
.getData());
}

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

TypeResolver in GraphQL Java

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:

public interface TypeResolver {


GraphQLObjectType getType(TypeResolutionEnvironment env);
}

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 Dog implements Pet {


name: String
owner: Person
doesBark: Boolean
}

type Cat implements Pet {


name: String
owner: Person
doesMeow: Boolean
}

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.

TypeResolver petTypeResolver = (env) -> {


Object javaObject = env.getObject();
if (javaObject instanceof Dog) {
return env.getSchema().getObjectType("Dog");
} else if (javaObject instanceof Cat) {
return env.getSchema().getObjectType("Cat");
} else {
return null;
}
};

This TypeResolver is then registered with the RuntimeWiring via a TypeRuntimeWiring,


similar to how DataFetchers are registered. This how to register the TypeResolver, as
well as the new DataFetcher wiring required for the Cat and Dog types.

// Lower level layer service does the work of retrieving data


PetService petService = new PetService();

DataFetcher<Pet> favoritePetDataFetcher = (env) -> {


return petService.getFavoritePet();
};

DataFetcher<Person> ownerDataFetcher = (env) -> {


// Load the correct Person for the Pet
Pet pet = env.getSource();
return petService.getPerson(pet.ownerId());
};

TypeResolver petTypeResolver = (env) -> {


Object javaObject = env.getObject();
if (javaObject instanceof Dog) {
return env.getSchema().getObjectType("Dog");
} else if (javaObject instanceof Cat) {
return env.getSchema().getObjectType("Cat");
} else {
return null;
}
};

69
DataFetchers

TypeRuntimeWiring queryWiring = newTypeWiring("Query")


.dataFetcher("favoritePet", favoritePetDataFetcher)
.build();

TypeRuntimeWiring petWiring = newTypeWiring("Pet")


.typeResolver(petTypeResolver)
.build();

TypeRuntimeWiring catWiring = newTypeWiring("Cat")


.dataFetcher("owner", ownerDataFetcher)
.build();

TypeRuntimeWiring dogWiring = newTypeWiring("Dog")


.dataFetcher("owner", ownerDataFetcher)
.build();

RuntimeWiring runtimeWiring = newRuntimeWiring()


.type(queryWiring)
.type(petWiring)
.type(catWiring)
.type(dogWiring)
.build();

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.

Spring for GraphQL

The Spring for GraphQL homepage is https://spring.io/projects/spring-graphql and the


source code can be accessed on GitHub at https://github.com/spring-projects/spring-
graphql.
The best way to get a project up and running with Spring for GraphQL is via the Spring
Initializr tool at https://start.spring.io. We previously created the application in the
“Your first Spring for GraphQL service” section of the Introduction chapter.
The Spring for GraphQL project contains multiple artifacts:
org.springframework.graphql:spring-graphql: Integrates GraphQL Java with the
Spring framework.
org.springframework.boot:spring-boot-starter-graphql: This is the Spring Boot
Starter for GraphQL.
org.springframework.graphql:spring-graphql-test: Testing support for Spring for
GraphQL. We’ll discuss this in the Testing chapter.
In addition to the Spring for GraphQL repo, some Boot-specific code is in the Spring
Boot repository1 .
When we write “Spring for GraphQL”, we always mean Spring for GraphQL with Spring
Boot.

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 WebFlux or Spring MVC

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

as a dependency. Spring for GraphQL automatically detects either dependency. In this


chapter, we will use WebFlux in the examples, but all examples can be easily changed to
their Spring MVC equivalent by removing the Mono and Flux types.

Reading schemas

Spring for GraphQL then scans for schema files in src/main/resources/graphql/


ending with *.graphqls or *.gqls. After the schema files are found and successfully
loaded, Spring for GraphQL exposes the GraphQL API at the endpoint /graphql by
default.

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.

Expanding our Spring for GraphQL service

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;

record Pet(String name, String color) {


}

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 shortcut annotation registers the pets() method as a DataFetcher


for the query field pets. We did not have to specify the schema field name, because
it was automatically detected from the method name. For more on @SchemaMapping

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.

Fetching data from an external service

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
}

Now we can query the owner’s name for each pet.

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;

record Pet(String name, String color, String ownerId) {


}

Add a class for Person, the type of the owner field:

package myservice.service;

record Person(String name) {


}

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

id: ID! # New field


name: String
color: String
owner: Person
}

type Person {
name: String
}

This is how to query the name of a specific pet with the id argument “123”.

query myFavoritePet {
pet(id: "123") {
name
}
}

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;

record Pet(String id, String name, String color, String ownerId) {


}

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
}

# New input type


input PetSearchInput {
namePattern: String
ownerPattern: String
}

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;

record PetSearchInput(String namePattern, String ownerPattern) {


}

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 mutation field


type Mutation {
changePetName(id: ID!, newName: String!): ChangePetNamePayload
}

# 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
}
}
}

Let’s add the mutation payload class.

package myservice.service;

record ChangePetNamePayload(Pet pet) {


}

Implement the mutation DataFetcher with the shortcut @MutationMapping annotation.

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.

Unions, interfaces, and TypeResolver

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
}

# Changed from type to interface


interface Pet {
id: ID!
name: String
color: String
}

# New implementation of Pet


type Dog implements Pet {
id: ID!

86
Building a GraphQL service

name: String
color: String
barks: Boolean
}

# New implementation of Pet


type Cat implements Pet {
id: ID!
name: String
color: String
meows: 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;

record Dog(String id,


String name,
String color,
boolean barks) implements Pet {
}

87
Building a GraphQL service

package myservice.service;

record Cat(String id,


String name,
String color,
boolean meows) implements Pet {
}

Let’s create a new Human class.

package myservice.service;

record Human(String name) {


}

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

Start your service and navigate to the GraphiQL interactive playground at


http://localhost:8080/graphiql. Try out various queries and mutations and
verify your code is working end to end.
In the chapters that follow, we will discuss more specialized topics such as schema design
and GraphQL errors. Later in the book, we will cover more advanced topics including a
deep dive into execution inside the GraphQL Java engine.

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

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

Then another response after 2 more seconds. And so on.

{
"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

Let’s implement subscriptions with Spring for GraphQL.


Start with a new Spring for GraphQL project with Spring Initializr at https://start.spring.io/,
as we did in the “Your first Spring for GraphQL service” section of the Introduction chap-
ter. Choose Spring for GraphQL as a dependency, and choose either Spring Web or Spring
Reactive Web as a dependency. In the following examples we will use Spring Reactive Web,
which includes Spring WebFlux. If you prefer to use subscriptions with WebMVC instead
of WebFlux, add org.springframework.boot:spring-boot-starter-websocket as a
dependency, and adjust the examples by removing Mono and Flux.
Let’s add a subscription field to a new schema.

# Every schema needs a Query type


type Query {
notUsed: String
}

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

Let’s also enable the GraphiQL playground for manual testing.

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
}

Figure 1: Subscription request

Then we will see the response changing every second, from “Hello 0” to “Hello 1” to
“Hello 2”, as shown in Figure 2.

Figure 2: Hello 2 answer

We have a working subscription. Congratulations!


If you instead saw this “isTrusted” error message, this is a generic message.

{
"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

Similarly to queries and mutations, we implement subscriptions as DataFetchers.


In order to deliver a stream of responses, GraphQL Java requires that it return a
org.reactivestreams.Publisher instance. The Reactive Streams1 initiative defines
this interface to provide a standard for asynchronous stream processing.
When using GraphQL Java with Spring, we use Flux from Project Reactor, which
implements Publisher. We used Flux in the example earlier in this chapter.
Spring for GraphQL takes care of bridging the transport layer WebSocket protocol to
this Publisher. Every time it emits a new result, we send it to the client.
The GraphQL specification requires a subscription request always has exactly one
root field, unlike queries and mutations. For example, the subscription below is invalid
because it has multiple roots.

# Not valid
subscription tooManyRoots {
newCats {
name
}
newDogs {
name
}
}

Additionally, we can only subscribe to one subscription per request.


We execute all sub-selections below the root field, as we would for a query or mutation.
Let’s return to our orders example from earlier in the chapter.

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.

Transport protocols and serialization

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:

public class ExecutionInput {


private final String query;
private final String operationName;
private final RawVariables rawVariables;
// and more 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

Over HTTP, the response to a GraphQL request is a JSON object:

{
"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.

public interface ExecutionResult {


<T> T getData();
List<GraphQLError> getErrors();
Map<Object, Object> getExtensions();
// and more methods
}

We will discuss GraphQL errors in greater detail in the next chapter.

HTTP status codes

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:

1. Check the HTTP status code


2. If the status code is 200 OK, check for any GraphQL errors in the response

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

We strongly recommend that information needed to understand the request should be


part of the GraphQL operation, and not passed in via request headers.
This general rule comes from the intention to express the API completely in the GraphQL
schema. Let’s demonstrate this via a counterexample, where an HTTP header “Customer-
Id” is sent alongside a query for the field ordersByCustomerId. In the schema, the field
is defined as:

type Query {
ordersByCustomerId: [Order]
}

Imagine you are reading the schema for the first time, wanting to understand the API.
There is no information to indicate that a “Customer-Id” header is essential for the
ordersByCustomerId field. The schema becomes an incomplete description of the API.
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

Spring for GraphQL provides WebGraphQlInterceptor to intercept requests.


Spring for GraphQL automatically handles the HTTP protocol, including the invocation
of GraphQL Java which actually executes the request. The default URL is /graphql,
which we can customize via the config property spring.graphql.path.
WebGraphQlRequest and WebGraphQlResponse represent the GraphQL request and re-
sponse over either HTTP or WebSocket. It contains GraphQL-specific information as
well as HTTP or WebSocket details.
We can access and change the request with WebGraphQlInterceptor. For example, we
can intercept a request and add a “special-header” to the response headers.

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");

request.configureExecutionInput((executionInput, builder) -> {


executionInput
.getGraphQLContext()
.put("beta-features", betaFeatures);
return executionInput;
});

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

intercept method, return your customized WebGraphQLResponse.


For example, if you already attach a request ID to incoming requests, it is useful to add
the request ID to the extensions section of the GraphQL response. This is very helpful
for debugging issues, as the person using your API can give you a request ID to quickly
find relevant logs.
This is how to append an extra key to the extensions of the 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");

return response.transform(builder ->


builder.extensions(newExtensions).build()
);
});

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:

• parsing errors, including GraphQL syntax errors


• validation errors in the GraphQL document, which typically indicates that the
request is not compliant with the GraphQL spec
• unable to determine which operation to execute
1
https://spec.graphql.org/draft/#sec-Errors.Request-Errors

106
GraphQL errors

• invalid input values for variables

For example, this request is invalid GraphQL syntax:

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]
}

We make a request with this query:

query whoIsAGoodPup {
favoritePet {
name
friends {
name
}
}
}

Let’s write the friends DataFetcher to intentionally fail, to simulate an error.

package myservice.service;

import java.util.List;

record Pet(String id, String name, List<String> friendIds) {


static List<Pet> pets = List.of(
new Pet("1", "Luna", List.of("2")),
new Pet("2", "Skipper", List.of("1"))
);
}

@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.

How errors appear in the response

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.

GraphQL Java provided error classifications

GraphQL Java provides commonly used error classifications in graphql.ErrorType.


These classifications implement the graphql.ErrorClassification interface.

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

Spring for GraphQL provided error classifications

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

If an exception is unresolved, it will be categorized by default as an INTERNAL_ERROR with


a generic message including the category name and executionId. You can customize
this with a DataFetcherExceptionResolverAdapter which will be discussed later in
this chapter.
GraphQL Java provided error classifications are also used in Spring for GraphQL. For
example, a request with invalid GraphQL syntax will contain a response error with the clas-
sification InvalidSyntax. Likewise, ValidationError, OperationNotSupported, and
NullValueInNonNullableField classifications are also used by Spring for GraphQL.
Note that an HTTP response code of 200 (OK) is always returned if the GraphQL engine
is invoked, even if there are errors in the response. The error classification is included in
the errors key of the GraphQL response. For example, if a database is unavailable, this
will cause a field error to be raised, and a GraphQL error will appear in the response.
The HTTP response code for a request with this database issue will still be 200. This
may seem surprising if you have experience with other APIs such as REST. See the
discussion on HTTP status codes in the previous chapter for a detailed explanation.

Custom error classifications

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.

How to return errors

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

Throw exception during DataFetcher invocation

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

Customizing exception resolution

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();
}
}

The Spring Boot starter automatically detects DataFetcherExceptionResolver beans as


part of instantiating GraphQlSource, the same step that also automatically loads schema
files and more. The abstract class DataFetcherExceptionResolverAdapter implements
DataFetcherExceptionResolver. In this example, CustomErrorMessageExceptionResolver
extends DataFetcherExceptionResolverAdapter and is annotated with @Component
to indicate it should be scanned by the Spring Boot starter.
When extending DataFetcherExceptionResolverAdapter, you have the choice of either
overriding the methods resolveToSingleError or resolveToMultipleErrors. As the
names suggest, a resolveToSingleError will resolve an exception to a single GraphQL
error, and resolveToMultipleErrors will resolve an exception to a list of GraphQL
errors.

115
GraphQL errors

Note that the DataFetcherExceptionResolverAdapter does not require you to use


Reactor types, as parts of the contract requiring Reactor types have already been
implemented for you.
The recommended way to create new instances of graphql.GraphQLError is via
graphql.GraphqlErrorBuilder. Please note the slightly different capitalization in
names. This builder is more convenient than implementing the graphql.GraphQLError
interface directly. Use the factory method that takes a DataFetchingEnvironment, as
shown in this example.
If you are using GraphQL Java without Spring for GraphQL, note that the handler in
GraphQL Java is different. GraphQL Java uses the SimpleDataFetcherExceptionHandler
implementation. This handler creates a ExceptionWhileDataFetching error with the
classification ErrorType.DataFetchingException.

Return data and errors with DataFetcherResult

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;

record Pet(String id, String name) {


static List<Pet> pets = List.of(
new Pet("1", "Luna"),
new Pet("2", "Skipper")
);
}

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));

GraphQLError error = GraphqlErrorBuilder.newError(env)


.errorType(ErrorType.DataFetchingException)
.message("Data could only be partially loaded")
.build();
return DataFetcherResult.<List<Pet>>newResult()
.data(result)
.error(error)
.build();
}

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

Schema design is a critical part of implementing a GraphQL service. In this chapter, we


will discuss key principles and best practices from our experiences running GraphQL
services.

Schema-first and implementation-agnostic

We should design our schema deliberately as a standalone activity. From experience,


we’ve seen it is not the best approach to infer a schema or generate it from another
source. Generated schemas will always be of lower quality.
GraphQL differs from a database schema or REST API that we might use to fetch data
for a query.
Implementation details should not influence schema design and should never be leaked.
We always need to ask: is this information really needed to be exposed and is it the right
format?
For example, just because the current database backing the API doesn’t allow the
User.name field to be null doesn’t automatically mean it should also be non-nullable in
the schema. The design needs to be justified independent of the current implementation.
If we build a GraphQL API and the implementation involves calling a user service via
REST, it doesn’t mean the user resource for a REST API is what we want to expose.
We should design a User type in GraphQL based on the requirements and then look into
how we can implement it.
Of course, we might need to adjust our ideal solution based on implementation constraints.
We need to be pragmatic and deliver a working API. The point remains that we should
always start with the schema first in order to design the best API.

Evolution over versioning

We prefer evolution over versioning as the recommended approach for GraphQL. In


REST APIs, we might see versions frequently, often as part of the URL like /v2/. In
a GraphQL API, there is only one URL and one schema that grows over time with
incremental changes.

118
Schema design

For example, we might start out with just a User.

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:

1. Introduce alternative (if applicable)


2. Deprecate old field
3. Monitor usage and/or wait a certain amount of time
4. Remove old field

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 detect that nobody is using that particular field


• you are certain that clients can handle the breaking change
• the change is for an internal company testing API, which is not used in production

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
}
}

This is the better, more connected schema.

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.

Schema elements are cheap

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!
}

If field d is null during execution for this query,

query myQuery {
a {
b {
c {
d
}
}
}
}

then we end up with this result.

{
"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
}

An interesting consequence of thinking about non-nullability in this way is that root


fields should always be nullable.
One common mistake with nullability is arguing based on the current implementation.
For example:

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:

• Root fields are always nullable


• Essential fields like id or key are non-nullable
• Elements inside lists probably are non-nullable
• All other fields probably should be nullable

Nullable input fields and arguments

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.

Pagination for lists with Relay’s cursor connection specification

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.

Relay’s cursor connections specification

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
}

Query and response

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
}
}
}

Requesting more pages

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
}
}
}

Key concepts of Relay’s cursor connections specification

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

A connection represents a page of data (edges), with additional metadata to enable


further page requests (pageInfo).

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

The edges of a connection represent a page of data. An edge is a wrapper object


that contains a data element (e.g. a Pet) and metadata. It is named in the format
<Entity>Edge. It must have at least two fields: the cursor for the current element and
the actual element named node. Optionally, additional fields can be added.

type PetEdge {
cursor: String!
node: Pet!
}

PageInfo

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.

Optional shortcut: adding nodes to a connection type

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:

• Payment details are invalid


• New user attempts to sign up with an existing user’s email address
• A notoriously unreliable external system is not available

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
}

union PetLookup = Pet | PetLookupError

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 .

• Name the field <verb><Entity>.


• The field has a single argument input: <verb><Entity>Input!.
• The type of the mutation response is <verb><Entity>Payload

For example, a mutation to create a user:

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.

• Fields, input fields, and argument names are “camelCase”: userName


• Types are “PascalCase”: UserName
• Enum values are capitalized “SNAKE_CASE”: FIRST_NAME

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.

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

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.

More DataFetcher inputs

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

In GraphQL Java, GraphQLContext is a mutable map containing arbitrary data, which


is made available to every DataFetcher. It provides a “global context” per execution.
In Spring for GraphQL, the global GraphQLContext can be accessed by adding it as a
method parameter to a schema mapping handler or batch mapping method. In pure
GraphQL Java, it can be accessed via ExecutionInput.getGraphQLContext(). For
example, let’s say we want to make a “userId” accessible to every DataFetcher. In Spring
for GraphQL, we can access GraphQLContext via a schema mapping or batch mapping
method parameter and add a userId:

@SchemaMapping
MyType myField(GraphQLContext context) {
context.put("userId", 123);
// Your logic here
}

It’s also possible to access and add to GraphQLContext via ExecutionInput. As we


saw in the Requests chapter, Spring for GraphQL provides an interface for intercepting
requests and accessing ExecutionInput.

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
}

In pure GraphQL Java, we can add userId to GraphQLContext via ExecutionInput or


via DataFetchingEnvironment:

ExecutionInput executionInput = ...;


executionInput.getGraphQLContext().put("userId", "123");

Note: the capitalisation of “L” is slightly different for the getter in DataFetchingEnvironment.

143
DataFetchers in depth

DataFetcher df = (env) -> {


env.getGraphQlContext().put("userId", "123");
...
}

Retrieve the userId value via the DataFetchingEnvironment.

DataFetcher df = (env) -> {


String userId = env.getGraphQlContext().get("userId");
...
}

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
}

We can directly query a customer as a root field or via an order.

query customerDetails {
customerById(id: "ID-1") {
contact {
name
}
}
}

or

query orderDetails {
order {
customer {
contact {
name
}
}
}
}

Add the following Java classes.

package myservice.service;

record Order(String id, String customerId) {


}

package myservice.service;

record Customer(String id, String contactId) {


}

package myservice.service;

record Person(String name) {


}

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

// DataFetcher for Query.order


OrderService orderService;
DataFetcher<DataFetcherResult<Order>> orderDf = (env) -> {
Order order = orderService.getOrder();
Person personForContact = order.getPersonForContact();
// Return data and a new local context
return DataFetcherResult.<Order>newResult()
.data(order)
.localContext(personForContact)
.build();
};

Then the DataFetcher for Customer.contact can make use of the pre-loaded Person in
local context, if it is available. If the Person is not available, a request to the Person
service will be made.

// DataFetcher for Customer.contact


PersonService personService;
DataFetcher<Person> contactDf = (env) -> {
// If we already loaded the person earlier
if (env.getLocalContext() instanceof Person) {
return env.getLocalContext();
}
Customer customer = env.getSource();
return personService.getPerson(customer.getContactId());
};

DataFetcher implementation patterns

There are a few considerations when implementing a DataFetcher:

• Does it involve I/O? (e.g. HTTP calls to another service)


• Is it computationally intensive?
• Should it be reactive or not?

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

A non-reactive DataFetcher can involve blocking I/O or computation work. Regardless


of the type of work, the structure is the same.

@Controller
record ThingController(DoSomeThing service) {
@SchemaMapping
MyType myField() {
return service.doSomething();
}
}

Or the same DataFetcher in pure GraphQL Java:

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.

Wrapping blocking I/O

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

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;

DataFetcher<CompletableFuture<MyType> df = (env) -> {


return CompletableFuture.supplyAsync(
() -> restTemplate.getForObject(url, MyType.class), // blocking call
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.

Spring for GraphQL Reactor support

Spring WebFlux is an asynchronous and non-blocking framework based on Reactor4 .


You can use Spring WebFlux together with Spring for GraphQL to build Reactor
DataFetchers.

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:

@SchemaMapping(type = "Foo", field = "bar")


Mono<String> bar() {
...
}

or

@SchemaMapping(type = "Foo", field = "bar")


Flux<String> bar() {
...
}

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;

// Set initial values for the Reactor context


@Component
class WebInterceptor implements WebGraphQlInterceptor {

@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.

Schema and operation 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
}

Every directive has a schema definition. As @deprecated is a built-in directive, this


definition is automatically added by every GraphQL implementation. You should not
manually add this definition to your schema.

directive @deprecated(reason: String = "No longer supported")


on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
| ENUM_VALUE

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

@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:

directive @skip(if: Boolean!)


on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @include(if: Boolean!)
on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT

The if argument is a Boolean indicating whether the @skip or @include directive is


active. The exclamation mark ! indicates that this argument is non-nullable.
Skipping (or not including) a field is like a request that doesn’t contain this field at all.
These queries produce the same result:

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:

query myQuery($someTest: Boolean!) {


experimentalField @include(if: $someTest)
}

@deprecated

@deprecated is a schema directive that can be used to mark fields, enum values, input
fields, and arguments as deprecated in the schema. It provides a structured way to
document deprecations. By default, the introspection API filters out deprecated schema
elements.
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:

directive @deprecated(reason: String = "No longer supported")


on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
| ENUM_VALUE

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
}

The @deprecated directive is used to automatically generate documentation. For example,


this is how the schema above appears in the documentation tab of GraphiQL. Click on
the book icon in the top left corner of the page, as shown in Figure 1.

Figure 1: Deprecated documentation in GraphiQL

157
Directives

@specifiedBy

@specifiedBy allows us to provide a scalar specification URL to describe the behavior


of custom scalar types.
Custom scalars are a powerful feature of GraphQL which enables the type system to be
extended. Initially in the GraphQL specification, custom scalars could only be defined in
the schema by name. For example:

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:

directive @specifiedBy(url: String!) on SCALAR

This is an example for a DateTime scalar:

scalar DateTime @specifiedBy(url:


"https://scalars.graphql.org/andimarek/date-time")

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

Defining your own schema and operation 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.

Defining schema directives

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:

# No arguments and can only be used on field definitions


directive @important on FIELD_DEFINITION

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:

# No arguments and can only be used on field definitions


directive @important on FIELD_DEFINITION

type Query @important { # Invalid usage


hello: String
}

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

# Can be used in two locations


directive @important on FIELD_DEFINITION | OBJECT

type Query @important { # Now it is valid


hello: String
}

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.

type @foo on SCHEMA | SCALAR | OBJECT |


FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE |
| UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

schema @foo { # Schema


query: Query
}

scalar CustomScalar @foo # Scalar

type Query @foo { # Object


hello(
arg: SomeInput @foo # Argument
): String @foo # Field definition
}

interface SomeInterface @foo { # Interface


hello: String
}

type SomeImplementation implements SomeInterface {


hello: String
}

union SomeUnion = SomeImplementation @foo # Union

enum SomeEnum @foo { # Enum

160
Directives

ENUM_VALUE @foo # Enum value


}

input SomeInput @foo { # Input object


inputField: String @foo # Input field
}

Whilst it is technically possible to define a directive that includes locations for both
schema and operation directives, in practice this is not common.

Defining operation directives

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.

# Can only be used on a field in a GraphQL document


directive @cache on FIELD

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

# Argument with a default value


directive @cache(maxAge: Int = 1000) on FIELD

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.

type @foo on QUERY | MUTATION | SUBSCRIPTION |


FIELD | FRAGMENT_DEFINITION | FRAGMENT_SPREAD |
INLINE_FRAGMENT | VARIABLE_DEFINITION

query someQuery(
$var: String @foo # Variable definition
) @foo # Query
{
field @foo # Field
... on Query @foo { # Inline fragment
field
}
...someFragment @foo # Fragment spread
}

fragment someFragment @foo { # Fragment


field
}

162
Directives

mutation someMutation @foo { # Mutation


field
}

subscription someSubscription @foo { # Subscription


field
}

Although it is technically possible to define a directive that includes locations associated


with schema and operation directives, in practice this is not common.

Repeatable directives

We can define schema and operation directives as repeatable, enabling it to be used


multiple times in the same location. If repeatable is not included in the directive
definition, the directive will be non-repeatable by default.
For example, a repeatable schema directive @owner:

directive @owner(name: String!) repeatable on FIELD_DEFINITION

type Query {
# Multiple owners per field possible
hello: String @owner(name: "Brian") @owner(name: "Josh")
}

Implementing logic for schema directives

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.

Changing execution logic with schema directives

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

directive @important(reason: String!) on FIELD_DEFINITION

And could be used in a schema on a field:

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.

GraphQLDirective importantDirective = schema.getDirective("important");


String name = importantDirective.getName();
List<GraphQLArgument> arguments = importantDirective.getArguments();
boolean repeatable = importantDirective.isRepeatable();
EnumSet<Introspection.DirectiveLocation> directiveLocations =
importantDirective.validLocations();

GraphQLDirective represents the definition of a directive, and GraphQLArgument repre-


sents an argument definition.
Then we have an instance of GraphQLAppliedDirective, which represents the usage of
the directive in a schema. In our example, we only have one usage.
In pure GraphQL Java, usage of our schema directive can be accessed via our instance of
GraphQLSchema called schema:

GraphQLSchema schema = ...;


GraphQLObjectType query = schema.getObjectType("Query");
GraphQLFieldDefinition hello = query.getFieldDefinition("hello");
GraphQLAppliedDirective appliedDirective
= hello.getAppliedDirective("important");
GraphQLAppliedDirectiveArgument reason
= appliedDirective.getArgument("reason");
String reasonValue = reason.getValue(); // "Being friendly"

164
Directives

In our example, the GraphQLAppliedDirective contains one GraphQLAppliedDirectiveArgument


for our one argument “reason” with the value “Being friendly”. To highlight
the difference, an instance of GraphQLDirective has GraphLArguments, and
GraphQLAppliedDirective has GraphQLAppliedDirectiveArguments. GraphQLDirective
represents the definition, and GraphQLAppliedDirective represents the usage of the
directive in the schema.
In practice, we often use schema directives inside a DataFetcher. For example, we could
use the @important directive like this in a Spring for GraphQL DataFetcher to change
how important fields are handled:

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";
}
}

Validation with schema directives

Directives can also be used for validation. For example, a @size schema directive for
arguments, which enforces a minimum quantity.

directive @size(min : Int = 0) on ARGUMENT_DEFINITION

It could be applied to an argument definition to validate if there are enough Applications


in the input:

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);
}
}

The Spring Boot starter automatically detects all RuntimeWiringConfigurer beans.

Adding metadata with schema directives

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:

directive @owner(name: String) on OBJECT

type User @owner(name: "Antoine") {


# More fields here
}

type Order @owner(name: "Felipe") {


# More fields here
}

type Payment @owner(name: "Stephan") {


# More fields here
}

A significant benefit of using directives is the ability to process them programmatically.


For example, we could create an automatic report, showing which types are owned by
whom. This ownership report example is more like a script rather than part of a Spring
for GraphQL service. Therefore, we’ll demonstrate this example in pure GraphQL Java.
GraphQL Java provides tools to programmatically visit schema elements. In pure
GraphQL Java code, we can map owners to types with the SchemaTraverser class, to-
gether with a GraphQLTypeVisitor. In this example, we will visit GraphQLObjectTypes.
There are many more options and hooks to customize visitors in GraphQL Java.

GraphQLSchema schema = ...;


Map<String, List<GraphQLObjectType>> ownerToTypes
= new LinkedHashMap<>();

SchemaTraverser schemaTraverser = new SchemaTraverser();

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.

directive @suffix(name: String) on OBJECT

type Dog @suffix(name:"__bark") {


name: String
}

type Cat @suffix(name: "__meow") {


name: String
}

With pure GraphQL Java, we can make use of schema transformer and type visitor
tools.

168
Directives

GraphQLSchema newSchema = SchemaTransformer.transformSchema(


schema, new GraphQLTypeVisitorStub() {

@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

transformation. To view a more complex example, please see graphql.util.Anonymizer


in GraphQL Java. This is a utility to help users of GraphQL Java anonymize their
schemas to provide realistic examples when reporting issues or suggesting improvements
to the maintainer team.

Implementing logic for operation 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:

directive @cache(maxAge: Int) on FIELD

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)
}

In GraphQL Java, operation directive definitions are represented as GraphQLDirectives.


Operation directive usages are represented as QueryAppliedDirectives. Note that the
word “query” here is misleading, as it actually refers to a directive that applies to any of
the three GraphQL operations: queries, mutations, or subscriptions. Operation directives
are still commonly referred to as “query” directives, hence the class name.
To explain how operation 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 @cache in Spring for GraphQL.
In pure GraphQL Java, we can access an operation directive’s definition in the schema
via an instance of GraphQLSchema, in this example called schema. We can access the
operation directive’s name, arguments, locations, and whether it is repeatable.

GraphQLSchema schema = ...;


GraphQLDirective cacheDirective = schema.getDirective("cache");
String name = cacheDirective.getName();
List<GraphQLArgument> arguments = cacheDirective.getArguments();

170
Directives

boolean repeatable = cacheDirective.isRepeatable();


EnumSet<Introspection.DirectiveLocation> directiveLocations
= cacheDirective.validLocations();

Usages of operation directives are represented in GraphQL Java as instances


of QueryAppliedDirective, and provided argument values are represented as
QueryAppliedDirectiveArgument.
We can access operation directives usage during execution via getQueryDirectives()
in DataFetchingEnvironment. For example:

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();

// Now we know the max allowed cache time and


// can make use of it
// Your logic here
}
// Your logic here

171
Directives

In this chapter, we covered directives, a powerful GraphQL feature that enables us to


change runtime execution and type validation. We covered the built-in directives and
how to write our own 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.

Initializing execution objects

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

return (builder) ->


builder.configureGraphQl(graphQlBuilder ->
// Here we can use `GraphQL.Builder`
// For example, executionIdProvider
graphQlBuilder
.executionIdProvider(new MyExecutionIdProvider()));
}
}

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);

DataFetcher<String> foo = (env) -> "foo";


TypeRuntimeWiring queryWiring = newTypeWiring("Query")
.dataFetcher("foo", foo)
.build();
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type(queryWiring)
.build();

GraphQLSchema schema = new SchemaGenerator()


.makeExecutableSchema(parsedSdl, runtimeWiring);
GraphQL graphQL = GraphQL.newGraphQL(schema).build();

How Spring for GraphQL starts execution

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.

GraphQL graphQL = GraphQL.newGraphQL(schema).build();

ExecutionInput request = ExecutionInput.newExecutionInput()


.query("query test {foo}")
.build();

ExecutionResult result = graphQL.execute(request);

Execution steps

In GraphQL Java, execution includes the following 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.

Parsing and validation

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:

query myQuery($name: String) {


echo(value: $name)
}

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).

Figure 1: Tree of Fields

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

Figure 2: Execution Order

Reactive concurrency-agnostic

GraphQL Java is reactive concurrency-agnostic. This means GraphQL Java doesn’t


prescribe a specific number of threads nor when they are used during execution.
This is achieved by leveraging java.util.concurrent.CompletableFuture. Every
DataFetcher can return a CompletableFuture, or if not, GraphQL Java wraps the
returned value into a CompletableFuture.
This enables GraphQL Java to invoke all the child DataFetchers of a field at once, similar
to this:

// All happens in the same thread.


// GraphQL Java doesn't create a new thread
// or use a thread pool.
List<CompletableFuture> dataFetchersCFs
= new ArrayList<CompletableFuture>();
for (DataFetcher df: childrenDFs) {
Object cf = df.get(env);
// Wrapping non-CF
if (!(cf instanceof CompletableFuture)) {
cf = CompletableFuture.completedFuture(cf);
}
dataFetchersCFs.add((CompletableFuture) cf);
}

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.

// Intensive compute work in the current thread


DataFetcher<String> df = (env) -> {
String str = intensiveComputeString();
return str;
};

// Still doing the actual work in the current thread


DataFetcher<CompletableFuture<String>> df = (env) -> {
String str = intensiveComputeString();
return CompletableFuture.completedFuture(str);
};

// Blocking I/O call


DataFetcher<String> df = (env) -> {
String str = makeHttpCallAndWaitForIt();
return str;
};

// Quick compute work in the current thread


DataFetcher<String> df = (env) -> {
return "Hello";
};

// Offloaded compute work on another thread


DataFetcher<CompletableFuture<String>> df = (env) -> {
Executor threadPool = ...;
return CompletableFuture.supplyAsync( () -> {
String str = intensiveComputeString();
return str;
},threadPool);
};

// Offloaded blocking I/O calls to another thread


DataFetcher<CompletableFuture<String>> df = (env) -> {
Executor threadPool = ...;
return CompletableFuture.supplyAsync( () -> {
String str = makeHttpCallAndWaitForIt();
return str;
},threadPool);
};

180
Execution

// Using a reactive API to make an HTTP call


DataFetcher<CompletableFuture<String>> df = (env) -> {
return makeReactiveHttpCall();
};

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
}

DataFetcher invalidDf = (env) -> {


return true; // Will cause an error during execution
}

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
}

type Dog implements Pet {


name: String
barks: Boolean
}

type Cat implements Pet {


name: String
meows: Boolean
}

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.

Instrumentation in Spring for GraphQL

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"
}
}
]
}

Writing a custom instrumentation

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()));
}
};
}
}

Let’s walk through this instrumentation. In the beginExecution hook, we return


an implementation of InstrumentationContext<ExecutionResult> with two methods
onDispatched and onCompleted. They are called when the execution starts and finishes.
We save the start time and then log the difference once the execution finishes. We’ll
discuss InstrumentationContext in more detail in the next section.
Spring for GraphQL automatically detects and registers the instrumentation. Apart from

186
Instrumentation

annotating this instrumentation with @Component, no further configuration is required.


At the time of writing, the latest Spring for GraphQL 1.1 uses GraphQL Java 19.x. In
Spring for GraphQL 1.2, GraphQL Java 20.x will be used, which adds the improved
SimplePerformantInstrumentation class. It is designed to be more performant and
reduce object allocations.
If you are using pure GraphQL Java, the instrumentation must be manually passed into
the graphql.GraphQL builder.

GraphQLSchema schema = ...;


GraphQL graphQL = GraphQL.newGraphQL(schema)
.instrumentation(new LogTimeInstrumentation())
.build();

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.

public interface InstrumentationContext<T> {

/**
* 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

Let’s discuss how state is managed in instrumentation.


We usually have only one graphql.GraphQL instance, which is reused across all requests.
Instrumentations are registered for each graphql.GraphQL instance. So how do we
maintain different instrumentation states per request? And how do we manage the state
for complex instrumentations that require coordination across multiple hooks?
State management is achieved with the createState method that is called once per
request and returns an InstrumentationState. InstrumentationState is an empty
Java interface representing any kind of state. This state is passed into every hook as
argument, and the state can be tracked across multiple hooks per request.
Let’s see this in action, with a more involved instrumentation. FieldCountInstrumentation
counts the number of fields executed for a request.

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;

import static graphql.execution.instrumentation

188
Instrumentation

.SimpleInstrumentationContext.noOp;

@Component
class FieldCountInstrumentation
extends SimpleInstrumentation {

static class FieldCountState implements InstrumentationState {


AtomicInteger counter = new AtomicInteger();
}

@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.

List<Instrumentation> chainedList = new ArrayList<>();


chainedList.add(new FooInstrumentation());
chainedList.add(new BarInstrumentation());

ChainedInstrumentation chainedInstrumentation
= new ChainedInstrumentation(chainedList);

GraphQLSchema schema = ...;


GraphQL graphQL = GraphQL.newGraphQL(schema)
.instrumentation(chainedInstrumentation)
.build();

Built-in instrumentations

For convenience, GraphQL Java contains 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.

List of instrumentation hooks

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

In this chapter we covered instrumentation, a mechanism to hook into GraphQL execution.


We covered how to create instrumentation in Spring for GraphQL that injects code to
observe the execution of a query, and instrumentation that changed runtime behavior.

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

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

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
}

A query could look like:

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;

record Person(String name, Integer bestFriendId) {


}

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.

Solving the n+1 problem

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();
}

// Modified controller method


@BatchMapping
List<Person> bestFriend(List<Person> people) {
List<Integer> ids = people
.stream()
.map(p -> p.bestFriendId())
.collect(Collectors.toList());
return personService.getPeopleById(ids);
}
}

Replace the @SchemaMapping annotation with @BatchMapping on the bestFriend


method. With @BatchMapping, by default the field name defaults to the method name
1
https://github.com/graphql-java/java-dataloader
2
https://github.com/graphql/dataloader

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);

DataLoader<Integer, User> userLoader = DataLoaderFactory


.newDataLoader(userBatchLoader);

// Usage
CompletableFuture<User> user1CF = userLoader.load(1);

196
DataLoader

CompletableFuture<User> user2CF = userLoader.load(2);


userLoader.dispatchAndJoin();

// Retrieve loaded users


User user1 = user1CF.get();
User user2 = user2CF.get();

In this example, we want to load users by ID (represented as an Integer value). We


want to load the users in a batch to make the data request efficient.
We first implement the BatchLoader Java interface by calling the loadUsersById method.
This is how our batched data will be fetched at the dispatch point. A BatchLoader must
return a CompletableFuture.
Then we create a new DataLoader instance via DataLoaderFactory, providing the
BatchLoader. Our DataLoader has argument types Integer and User, indicating we
have a mapping from an Integer value to a User. DataLoader is based on the idea that
we have a key (in this case Integer) being mapped to an entity (User). That means we
can load an entity with a given key.
This completes the setup and then we can start using DataLoader.load. We immediately
return these two calls with a CompletableFuture, but note that no actual loading has
happened yet.
Then we want to batch load the users. This dispatch step is triggered with
DataLoader.dispatchAndJoin(). This is a manual way to tell the DataLoader instance
that it is time to commence batch loading. Note that in later examples, the dispatch
point will be managed by GraphQL Java.
dispatchAndJoin triggers the invocation of userBatchLoader with the two user IDs 1
and 2 as arguments. After dispatchAndJoin returns, we have loaded all users and we
can access them.
By default, DataLoader uses a simple in-memory java.util.Map as a cache to prevent
multiple entities being loaded twice. So another userLoader.load(1) would return a
User from this cache.
We created a DataLoader instance via DataLoaderFactory, which allows for multiple
options to be configured via DataLoaderOptions. For example, we can change the cache
implementation or disable caching completely.

DataLoader and GraphQL Java

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;

import static graphql.ExecutionInput.newExecutionInput;


import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import static java.util.concurrent.CompletableFuture.completedFuture;

public class PureGraphQLJava {

public static void main(String[] args) throws Exception {


// Set up schema
String sdl = """
type Query {
people: [Person]
}

type Person {
name: String
bestFriend: Person
}

198
DataLoader

""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().parse(sdl);

PersonService personService = new PersonService();


DataFetcher<List<Person>> people = (env) -> personService
.getAllPeople();

// Set up Person BatchLoader


BatchLoader<Integer, Person> personBatchLoader = personIds ->
completedFuture(personService.getPeopleById(personIds));

// DataFetcher implementation: select correct DataLoader and use


// it to load the data
String PERSON_DATA_LOADER = "person";

DataFetcher<CompletableFuture<Person>> bestFriendDF = (env) -> {


Person person = env.getSource();
DataLoader<Integer, Person> dataLoader
= env.getDataLoader(PERSON_DATA_LOADER);
return dataLoader.load(person.bestFriendId());
};

// Make executable schema


TypeRuntimeWiring queryWiring = newTypeWiring("Query")
.dataFetcher("people", people)
.build();
TypeRuntimeWiring bestie = newTypeWiring("Person")
.dataFetcher("bestFriend", bestFriendDF)
.build();
RuntimeWiring runtimeWiring = newRuntimeWiring()
.type(queryWiring)
.type(bestie)
.build();
GraphQLSchema schema = new SchemaGenerator()
.makeExecutableSchema(parsedSdl, runtimeWiring);

// Per request:

// Creating the DataLoader


DataLoader<Integer, Person> personDataLoader = DataLoaderFactory
.newDataLoader(personBatchLoader);

// Adding DataLoader instance to a DataLoaderRegistry

199
DataLoader

DataLoaderRegistry registry = new DataLoaderRegistry();


registry.register(PERSON_DATA_LOADER, personDataLoader);

String query = """


query everyone {
people {
name
bestFriend {
name
}
}
}""";

// Providing DataLoader instance to the ExecutionInput


ExecutionInput executionInput = newExecutionInput()
.query(query)
.dataLoaderRegistry(registry)
.build();

// Execute query
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
ExecutionResult executionResult = graphQL.execute(executionInput);
}
}

The Person DataLoader is registered with the bestFriend DataFetcher.


Note that a new DataLoader instance needs to be created per request, as it holds state
about what has been dispatched and cached, and this should not be shared across
requests.
As there are often multiple entities we want to use with DataLoader, there can be multiple
different DataLoader instances per request. These are managed by DataLoaderRegistry,
which identifies each DataLoader instance by a name. It is also possible to dispatch all
registered DataLoader instances at once with DataLoaderRegistry.dispatchAll.
We then create an instance of ExecutionInput, which represents the incoming GraphQL
request. See the Request chapter for more on ExecutionInput. The request is then
executed and returns the result.
It is important to highlight that there is no “dispatch” in this pure GraphQL Java
code. This is because GraphQL Java knows when to dispatch with the help of the
DataLoaderDispatcherInstrumentation, which is automatically added to the GraphQL
instance.

200
DataLoader

DataLoader and Spring for GraphQL

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;

// Registering the BatchLoader


batchLoaderRegistry
.forTypePair(Integer.class, Person.class)
.registerBatchLoader(
(integers, batchLoaderEnvironment) -> Flux
.fromIterable(personService.getPeopleById(integers)
)
);
}

201
DataLoader

@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}

// Manually using DataLoader instance in DataFetcher


@SchemaMapping
CompletableFuture<Person> bestFriend(
Person person, DataLoader<Integer, Person> dataLoader) {
// Using the DataLoader
return dataLoader.load(person.bestFriendId());
}

BatchLoaderRegistry is a map of names to BatchLoaders. Spring for GraphQL auto-


matically chooses the name to be the full class name of the entity we want to load. This
name can be customized. Note that the BatchLoaderRegistry expects BatchLoaders
to return a Flux and not a CompletableFuture.
Based on the automatically chosen BatchLoader name, we can declare DataLoader<Integer,
Person> dataLoader as a method argument and Spring knows which DataLoader to
inject based on the types <Integer, Person>.
In our example, the bestFriend DataFetcher only delegates directly to the DataLoader.
This allows us to further reduce our code by using the @BatchMapping annotation, as we
saw earlier in this chapter.

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;

// This constructor is written to emphasize


// Spring for GraphQL automation.

202
DataLoader

// You can instead use a record class


// and use the generated constructor.
PersonController(PersonService personService) {
this.personService = personService;
// No longer require BatchLoaderRegistry
// nor manual BatchLoader registration
}

@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}

// Using @BatchMapping instead of @SchemaMapping


@BatchMapping
List<Person> bestFriend(List<Person> people) {
List<Integer> ids = people
.stream()
.map(p -> p.bestFriendId())
.collect(Collectors.toList());
return personService.getPeopleById(ids);
}

The @BatchMapping annotation helps us remove much of the boilerplate code.


It is important to note that Person in this final example is the key type for the DataLoader
signature rather than Integer. The signature is now DataLoader<Person, Person>
rather than DataLoader<Integer, Person>. Therefore, it is critical that Person imple-
ments equals and hashcode methods in order to work as a key.
Spring for GraphQL automatically registers a BatchLoader with the type pair Person,
Person in the BatchLoaderRegistry, so we no longer need to manually do this step. Note
that Person is both the key and value type in this automatically registered BatchLoader.
The BatchLoader logic is moved to the last line of the bestFriend method.
Then Spring for GraphQL creates a DataLoader instance with the full class name of
Person. In our bestFriend method, a list of people is provided as an argument. We then
extract the best friend IDs and delegate to the DataLoader instance, which is analogous
to dataLoader.load(person.bestFriendId) in the previous example.

203
DataLoader

@BatchMapping method signature

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

The supported return types are:

Return Type Description


Mono<Map<K, V>> The mapping from key to value
Flux<V> A reactive sequence of resolved values
Map<K, V> Non-reactive map of key to value
Collection<V> Non-reactive collection (including list)
Callable<Map<K,V>>, Imperative variant to be invoked asynchronously
Callable<Collection<V>>

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.

Unit testing DataFetcher

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
}

And a simple DataFetcher registered with the @QueryMapping controller annotation.

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;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(MockitoExtension.class)
class GreetingControllerTest {

@Test
void testHelloDataFetcher() {
GreetingController greetingController = new GreetingController();

String greeting = greetingController.hello();


// Verify returned object is expected one
assertThat(greeting).isEqualTo("Hello, world!");

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

GraphQlTester is the primary class to help us test in Spring for GraphQL.


GraphQlTester is a Java interface with a few inner interfaces, which provides a rich API
to execute requests and verify responses. There are a number of implementations for
different types of tests:

• HttpGraphQlTester for HTTP-based testing, leveraging a WebTestClient


• WebGraphQlTester for testing a WebGraphQlHandler
• ExecutionGraphQlServiceTester for testing a ExecutionGraphQlService
• WebSocketGraphQlTester for testing WebSocket requests

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

A document is provided with document.

208
Testing

Alternatively, we could use GraphQlTester.documentName to specify a resource .graphql


file containing a document, instead of using an inline string. By default, GraphQlTester
expects the file to be in src/test/resources/graphql-test.
For example, you could save this query in a file called greeting.graphql in the directory
src/test/resources/graphql-test.

# Save this file as "greeting.graphql" in src/test/resources/graphql-test


query greeting {
hello
}

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!");

GraphQlTester.Request and execute

GraphQlTester.document returns a GraphQlTester.Request.


GraphQlTester.Request specifies the details of a request (operation name, variables)
and then GraphQlTester.Request.execute returns a GraphQlTester.Response.

GraphQlTester.Response, path, entity, entityList

GraphQlTester.Response extends Traversable, which is a simple interface:

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

You can traverse a GraphQL response by providing a path and returning a


GraphQlTester.Path, which is again itself a Traversable. A Path has different ways
of asserting or converting the current part of the GraphQL response. We can convert it
to an entity or entityList and then assert further.
For example, a Pet can be converted to an entity and then asserted further. For example,
a very basic favorite Pet schema:

type Query {
favoritePet: Pet
}

type Pet {
name: String
}

With a small Pet class:

package myservice.service;

record Pet(String name) {


}

And a basic DataFetcher that queries a database:

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

Pet favoritePet = graphQlTester


.document("query whoIsAGoodBoyOrGirl { favoritePet { name } }")
.execute()
.path("favoritePet")
.entity(Pet.class)
.get()
// Your assertion here

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!");

To filter out particular errors, we can use errors().filter(error -> ...).verify()


to exclude errors from the test. To verify that an error is present, we can use
.errors().expect(error -> ...).verify().

Testing different layers

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

1. A general purpose HTTP request invokes GraphQlHttpHandler converts the request


into a WebGraphQlRequest.
2. WebGraphQlHandler takes the WebGraphQlRequest, and calls ExecutionGraphQlService
to execute the request.
3. ExecutionGraphQlService ultimately invokes GraphQL Java.

Figure 1: Spring for GraphQL classes

We can test these different layers:

• 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.

End-to-end over HTTP

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)

As mentioned earlier in this chapter, we have a HttpGraphQlTester to test a GraphQL


API via HTTP. It builds on top of WebTestClient.
By combining the @SpringBootTest annotation and the HttpGraphQlTester, we can
write an end-to-end test:

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

A WebGraphQlHandler test enables direct testing of WebGraphQlHandler. This includes


WebGraphQlInterceptor, because the WebGraphQlHandler manages interceptors. Cre-
ate a new tester instance by providing the relevant WebGraphQlHandler.

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

ExecutionGraphQlServiceTester enables direct testing of ExecutionGraphQlService.


Create a new tester instance by providing the relevant ExecutionGraphQlService.

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!");
}

Focused GraphQL testing with @GraphQlTest

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
}

The query we want to test is:

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

GraphQlTester offers an executeSubscription method that returns a GraphQlTester.Subscription.


This can be then further converted to a Flux and verified. To test Flux more easily, add
the Reactor testing library io.projectreactor:reactor-test.
Testing our hello subscription from the Subscription chapter end to end looks like this.

# Every schema needs a Query type


type Query {
notUsed: String
}

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

If you haven’t already, set the WebSocket path in application.properties.

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();
}
}

We create a WebSocketGraphQlTester by proving the URL and a WebSocket client. The


default WebFlux server is Netty, meaning we use the ReactorNettyWebSocketClient.
In the actual test, we execute the subscription request and convert it to a Flux<String>
by selecting the hello values from each response. Then we use the StepVerifier from
the Reactor testing library to verify that we get exactly the three events we expect.
The same way we have tested subscription end to end here also allows us to test
subscriptions on different layers.

Testing recommendations

A general guide for writing good tests is to have the smallest or most focused test possible
that verifies what we want to test.
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.

Securing a Spring for GraphQL service

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;

record Order(String id, String details, String owner) {


}

package myservice.service;

import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
class OrderService {

private final List<Order> orders;

OrderService() {
// A mutable list of orders
this.orders = new ArrayList<>(List.of(
new Order("1", "Kibbles", "Luna"),
new Order("2", "Chicken", "Skipper"),

224
Security

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")
));
}

List<Order> getOrdersByOwner(String owner) {


return orders
.stream()
.filter(order -> order.owner().equals(owner))
.collect(Collectors.toList());
}

boolean deleteOrder(String orderId) {


return orders.removeIf(order -> order.id().equals(orderId));
}

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);
}
}

Let’s step through this security configuration:

• 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;

record DeleteOrderInput(String orderId) {


}

package myservice.service;

record DeleteOrderPayload(boolean success) {


}

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

Figure 1: Luna’s orders

230
Security

Figure 2: Luna is unauthorized to delete orders

Spring for GraphQL support for 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

Spring Security offers method-level annotations to perform authorization in a declarative


way. As Spring for GraphQL integrates Spring Security, we can use these annotations to
refactor OrderService.

@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());
});
}

We need to change the return type to Mono because ReactiveSecurityContextHolder


itself is reactive. Finally, we need to add @EnableReactiveMethodSecurity to the
Config class to enable these annotations.
Putting it all together, here is the full source code for Config, Controller, and
OrderService.

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 {

private final List<Order> orders;

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)));
}

Note that all authorization logic is now contained in the OrderService.


In addition to @PreAuthorize, Spring Security also offers @PostAuthorize, @PreFilter,
and @PostFilter to perform authorization and filtering before and after method invoca-
tion.

235
Security

Testing auth

For an introduction to testing, please see the previous chapter on Testing.


Let’s write end-to-end auth tests. To start with the simplest test and establish a good
baseline, let’s verify that we reject unauthenticated requests.

package myservice.service;

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")));

SecurityContext context = SecurityContextHolder.getContext();


context.setAuthentication(authenticated);
return chain
.next(request)
.contextWrite(
ReactiveSecurityContextHolder
.withSecurityContext(Mono.just(context)
));
}
}

@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

The HTTP GraphQL client is basically a wrapper around a WebClient, so we need to


provide a WebClient when creating a HttpGraphQlClient.

package myservice.service;

import org.springframework.graphql.client.HttpGraphQlClient;
import org.springframework.web.reactive.function.client.WebClient;

WebClient webClient = ... ;


HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

Using builder methods, we can provide specific HTTP settings, such as headers at build
time.

HttpGraphQlClient graphQlClient = HttpGraphQlClient


.builder(webClient)
.header("Special-Header", "true")
.build();

or

240
Java client

HttpGraphQlClient graphQlClient = HttpGraphQlClient


.builder(webClient)
.headers(httpHeaders -> httpHeaders.setBearerAuth("token"))
.build();

We can also provide other settings such as url.

HttpGraphQlClient graphQlClient = HttpGraphQlClient


.builder(webClient)
.url("/public/graphql")
.build();

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.

HttpGraphQlClient newClient = graphQlClient


.mutate()
.header("Another-Header", "false")
.build();

WebSocket client

The WebSocketGraphQlClient uses a WebSocketClient under the hood. We have to


provide a WebSocketClient when creating a new WebSocketGraphQlClient. Note that
WebSocketClient is an abstraction with implementations for Reactor Netty, Tomcat
and others1 .
Here is an example using a Netty-based implementation.

import org.springframework.graphql.client.WebSocketGraphQlClient;
import org.springframework.web.reactive.socket.client
.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;

String url = "http://localhost:8080/graphql";


WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient
.builder(url, client)
.build();

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.

WebSocketGraphQlClient newClient = graphQlClient


.mutate()
.url("https://my-new-url")
.build();

GraphQlClient

We can only use GraphQlClient after creating an instance of an HTTP or WebSocket


client. In the following examples, graphQlClient could be either an HTTP or WebSocket
client.
Use document or documentName to define the request operation and then we can retrieve
specific parts of the response. Note that this is similar in design to GraphQlTester.

Mono<List<Pet>> pets = graphQlClient


.document("query petNames {pets{name}}")
.retrieve("pets")
.toEntityList(Pet.class);

If we have a subscription request, we use retrieveSubscription to get Flux instead of


a Mono.

Flux<Pet> pets = graphQlClient


.document("subscription newPetNames {newPet{name}}")
.retrieveSubscription("newPet")
.toEntity(Pet.class);

As an alternative to retrieve, we can use execute and executeSubscription to return


a Mono<ClientGraphQlResponse> or Flux<ClientGraphQlResponse> respectively. A
ClientGraphQlResponse allows access to the full GraphQL response, including GraphQL
errors.

Mono<List<Pet>> pets = graphQlClient


.document("query {pets{name}}")
.execute()
.map(response -> {

242
Java client

List<ResponseError> errors = response.getErrors();


// Your additional code here
});

In this chapter, we covered Spring for GraphQL’s built-in GraphQlClient, for making
GraphQL requests over HTTP or WebSocket.

Ещё больше книг по Java в нашем телеграм канале:


https://t.me/javalib

243

You might also like