GraphQL With Java and Spring
GraphQL With Java and Spring
GraphQL With Java and Spring
Andreas Marek
Donna Zhou
GraphQL with Java and Spring
GraphQL with Java and Spring
Prologue
Andi (Andreas)
Donna
About this book
Introduction
Your first Spring for GraphQL service
What is GraphQL?
A brief history of GraphQL
From GraphQL Java to Spring for GraphQL
Overview
Three layers
Schema and SDL
GraphQL query language
Request and Response
Execution and DataFetcher
How concepts relate to each other
GraphQL Java
Spring for GraphQL
Schema
Schema-first
Loading schema resources in Spring for GraphQL
GraphQL schema elements
GraphQL types
Fields everywhere
Scalar
Enum
Object
Input object
Interface
Union
List and NonNull
Directives
Arguments
Documentation with descriptions
Comments
GraphQL query language
Literals
Operations
Query operations
Mutation operations
Subscription operations
Arguments
Fragments
Inline fragments
Variables
Aliases
GraphQL document
Named and unnamed operations
Query language in GraphQL Java
DataFetchers
Spring for GraphQL annotated methods
PropertyDataFetchers in Spring for GraphQL
DataFetchers and schema mapping handler methods
TypeResolver in Spring for GraphQL
Arguments in Spring for GraphQL
More Spring for GraphQL inputs
Adding custom scalars in Spring for GraphQL
Under the hood: DataFetchers inside GraphQL Java
DataFetchers in GraphQL Java
Source objects in GraphQL Java
RuntimeWiring in GraphQL Java
Creating an executable schema in GraphQL Java
TypeResolver in GraphQL Java
Building a GraphQL service
Spring for GraphQL
GraphQL Java
Spring WebFlux or Spring MVC
Reading schemas
Configuration properties
Expanding our Spring for GraphQL service
Pet schema
Controllers
Fetching data from an external service
Source object
GraphQL arguments
Mutations
Unions, interfaces, and TypeResolver
Subscriptions
Getting started
Execution
Protocol
Client support
Request and response
Transport protocols and serialization
Request
Response
HTTP status codes
HTTP headers
Intercepting requests
GraphQL errors
Request errors
Field errors
How errors appear in the response
Error classifications
How to return errors
Throw exception during DataFetcher invocation
Customizing exception resolution
Return data and errors with DataFetcherResult
Schema design
Schema-first and implementation-agnostic
Evolution over versioning
Connected
Schema elements are cheap
Nullable fields
Nullable input fields and arguments
Pagination for lists with Relay’s cursor connection specification
Relay’s cursor connections specification
Schema
Query and response
Requesting more pages
Key concepts of Relay’s cursor connections specification
Expected errors
Mutation format
Naming standards
DataFetchers in depth
More DataFetcher inputs
Global context
Local context
DataFetcher implementation patterns
Spring for GraphQL Reactor support
Directives
Schema and operation directives
Built-in directives
@skip and @include
@deprecated
@specifiedBy
Defining your own schema and operation directives
Defining schema directives
Defining operation directives
Repeatable directives
Implementing logic for schema directives
Changing execution logic with schema directives
Validation with schema directives
Adding metadata with schema directives
Implementing logic for operation directives
Execution
Initializing execution objects
How Spring for GraphQL starts execution
Execution steps
Parsing and validation
Coercing variables
Fetching data
Reactive concurrency-agnostic
Completing a field
TypeResolver
Query vs mutation
Instrumentation
Instrumentation in Spring for GraphQL
Writing a custom instrumentation
InstrumentationContext
InstrumentationState
ChainedInstrumentation
Built-in instrumentations
List of instrumentation hooks
DataLoader
The n+1 problem
Solving the n+1 problem
DataLoader overview
DataLoader and GraphQL Java
DataLoader and Spring for GraphQL
@BatchMapping method signature
Testing
Unit testing DataFetcher
GraphQlTester
document or documentName
GraphQlTester.Request and execute
GraphQlTester.Response, path, entity, entityList
errors
Testing different layers
End-to-end over HTTP
Application test
WebGraphQlHandler test
ExecutionGraphQlService test
Focused GraphQL testing with @GraphQlTest
Subscription testing
Testing recommendations
Security
Securing a Spring for GraphQL service
Spring for GraphQL support for security
Method security
Testing auth
Java client
HTTP client
WebSocket client
GraphQlClient
GraphQL with Java and Spring
Prologue
Andi (Andreas)
In 2015, I (Andi) was working as a software developer for a small company
in Berlin, Germany. During a conversation, one of my colleagues (thanks a
lot Stephan!) mentioned to me this new technology called “GraphQL”,
aimed at improving the way clients access data from a service, and they
planned to release it soon.
While it was just me in the beginning, shortly after I received the first PR
that fixed a typo. Today more than 200 people 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 Baker, who has been a
co-maintainer for over six years. There is no way to overstate his
contributions and influence on GraphQL Java. It is as much his project as it
is mine.
Most importantly I want to thank my wife Elli for all her support: without
her there would be no book today.
Donna
I (Donna) am thrilled to write this book with Andi, who created GraphQL
Java and played a major role in the creation of Spring for GraphQL. Andi,
Brad, and I are the maintainers of GraphQL Java.
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.
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.
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 “Spring Start”.
Spring Start
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.
type Query {
pets: [Pet]
}
type Pet {
name: String
color: String
}
package myservice.service;
Next, we’ll add the logic to connect our schema with the pet data. Create a
new Java class PetsController in the package myservice.service.
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class PetsController {
@QueryMapping
List<Pet> pets() {
return List.of(
new Pet("Luna", "cappuccino"),
new Pet("Skipper", "black"));
}
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 GraphiQL
interactive 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 the screenshot “GraphiQL request and response”.
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.
Then click on the root Query for pets, and then click on Pet to see its
attributes (name and color), as shown in the screenshot “GraphiQL Pet
documentation”.
What is GraphQL?
GraphQL is a technology for client-server data exchange. The typical uses
cases are web or mobile clients accessing or changing data on a backend
server, as shown in the diagram “Client Server”. We sometimes describe the
two parties involved as the API “consumer” and “producer”.
Client Server
# This is a comment
# We are asking for two fields:
# "pets" and the "name" for each pet
query myPets {
pets {
name
}
}
{
"data":
{
"pets": [
{
"name": "Luna"
},
{
"name": "Skipper"
}
]
}
}
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.
{
"pets": [
{
"name": "Luna",
"color": "cappuccino"
},
{
"name": "Skipper",
"color": "black"
}
]
}
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 RSocket, 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.
For example, the API used by the queries above would look like this:
# This is a comment
This syntax is called Schema Definition Language (SDL) and the structure
of a GraphQL API is called a schema. Every GraphQL API has a schema
that clearly describes the API in SDL syntax. The best way to think about a
GraphQL API for now is that it is a list of types with a list of fields.
Every GraphQL API offers special fields that let you query the schema of
the API itself. This feature is called introspection. For example, a valid
query for every GraphQL API is this:
query myIntrospection {
__schema {
types {
name
}
}
}
As you can see the special field __schema starts with __, which indicates
this is an introspection field, not a normal field.
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 GraphQL:
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 2015. 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
Foundation. The GraphQL Foundation is a vendor-neutral entity,
comprising over 25 members. The list includes AWS, Airbnb, Atlassian,
Microsoft, IBM, and Shopify. The official description of the foundation is:
The GraphQL Foundation is a neutral foundation founded by global
technology and application development companies. The GraphQL
Foundation encourages 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 most important group for developing the GraphQL spec is the
GraphQL Working Group (often shortened to WG). It is an open group that
meets online three times a month and mainly discusses GraphQL spec
changes and improvements. Everybody from the GraphQL community can
join. More details are available in the working group GitHub repository.
So some time after I released GraphQL Java, the first GraphQL Java Spring
integrations became available. I even developed a small GraphQL Java
Spring library, which aimed to be as lightweight as possible. But nothing
beats an official Spring integration maintained by the Spring team that
allows for the most GraphQL adoption and best experience overall.
In July 2020, the Spring and GraphQL Java teams came together to develop
an official Spring for GraphQL integration. One year later, we published a
first milestone and after that, the first release of Spring for GraphQL in May
2022.
Spring for GraphQL comprises two parts: one is the actual Spring
Framework integration with GraphQL Java, and on top of that there is the
Spring Boot Starter 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 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.
Overview
In this chapter, we cover the fundamentals aspects of Spring for GraphQL
and GraphQL Java, and how they relate to each other. This is important to
build an overall understanding and not get lost in the details in the next
chapters.
Three layers
We have three layers to consider, where the higher ones depend on the
lower ones.
Three layers
We can consider GraphQL Java and the spec as the same from a practical
Java point of view. GraphQL Java doesn’t offer major features beyond the
spec and the spec doesn’t define anything that is not represented in Java, it
is a one-to-one relationship. Therefore, we will not discuss the spec
separately from GraphQL Java, and we can assume features in GraphQL
Java by default to be defined in the spec.
type 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.
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.
{
"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.
The response can contain data, errors, and extensions. Data can be anything.
In ExecutionResult, data is Map<String, Object>. On the transport
layer, we send the response over HTTP in JSON.
The first step is purely Spring and can also involve aspects like
authentication. Then Spring invokes GraphQL Java and once finished, the
response is again handled by Spring and sent back to the client.
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 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.
How concepts relate to each other
Concept relations
GraphQL Java
The primary classes of GraphQL Java are, as shown in the diagram
“GraphQL Java classes”:
A request passes through three primary classes in Spring for GraphQL, each
with a distinct responsibility, as shown in the diagram “Spring for GraphQL
classes”:
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.
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.
Under the hood, Spring for GraphQL automatically reads the schema files,
parses the files, and instantiates GraphQL Java’s GraphQLSchema object
with the schema.
GraphQL types
The most important schema elements are the types. There are eight types in
the GraphQL type system: Object, Interface, Union, Enum, Scalar,
InputObject, List, and NonNull. The first six are “named types”, because
each type has a unique name across the whole schema, while List and
NonNull are called “wrapping types”, because they wrap named types, as
we will see later.
Another classification of types differentiates between input and output
types. An output type is a type that describes the result of a request, while
input types are used to describe input data for a GraphQL request. We will
cover requests in greater detail in the next chapter on GraphQL query
language.
Fields everywhere
The most prominent elements of a schema are fields. Objects and interfaces
contain fields which can have arguments. An input object has input fields,
which cannot have arguments. If we squint, we can think of a schema as a
list of types with a list of fields.
GraphQL comes with five built-in scalars: String, Int, Float, Boolean,
and ID. In addition, every GraphQL service can define its own custom
scalars.
type Pet {
"String is a built-in scalar, therefore no declaration
name: String
dateOfBirth: Date
}
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.
enum PetKind {
CAT, DOG, BIRD
}
type Pet {
name: String
kind: PetKind # used as output type
}
type Query {
pets(kind: PetKind!): [Pet] # used as input type
}
Alternatively, enums can also be declared with each value on its own line.
This is because a comma , is considered whitespace and is ignored.
enum PetKind {
CAT
DOG
BIRD
}
Object
A GraphQL object type describes a certain shape of data as a list of fields.
It has a unique name across the schema. Each field has a specific type,
which must be an output type. Every field has an optional list of arguments.
Recursive references are allowed.
type Person {
name: String
}
type Query {
pet(name: String!): Pet # lookup a pet via name
}
Input object
An input object type describes a group of input fields where each has an
input type. An input object name must be unique across the schema.
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.
interface Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
}
# Another implementation
type Cat implements Pet {
name: String
owners(includePreviousOwners: Boolean): [Person!]
doesMeow: Boolean # additional field specific to Cat
}
It might surprise you that all interface fields must be repeated. As we can
see in this example, Cat and Dog both repeat the same name field from Pet.
This was a deliberate decision by the GraphQL working group to focus
more on readability than shorter notation.
Union
A union type must be one of the member types at execution time. In other
words, a union is fully described by the list of possible object types it can
be at execution time.
type Dog {
name: String
doesBark: Boolean
}
type Cat {
name: String
doesMeow: Boolean
}
A list type is a list of the wrapped type. A non-null type marks this type as
never being null.
type Query {
pet(id: ID!): Pet
}
type Pet {
id: ID!
ownerNames: [String!] # A combination: a list of non-nu
}
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.
# Example usage
type SomeType {
field(arg: Int @example): String @example
}
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]
}
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
}
Documentation in GraphiQL
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 specification 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:
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.
GraphQL query language
The GraphQL query language is the domain-specific language (DSL) that
enables consumers 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.
Literals
The query language contains several literals that mirror the schema input
types.
Operations
There are three operations in GraphQL:
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.
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.
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.
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.
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
}
}
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.
mutation myMutation {
changeSomething
}
type Mutation {
changeUser(newName: String!): User
}
type User {
name: String
address: Address
}
type Address {
street: String
country: String
}
After changing the user’s name to “Brad”, we can query the details of the
changed user as a normal query. Notice how the sub-selection looks exactly
like a query sub-selection.
mutation changeUserName {
changeUser(newName: "Brad") {
name
address {
street
country
}
}
}
If there are two or more root fields in the mutation operation, they will be
executed in sequence, as required by the GraphQL spec.
mutation mutationWithTwoRootFields {
first: changeName(name: "Bradley")
second: changeName(name: "Brad")
}
In this example, the final name of the user will always be “Brad”, because
the fields are always executed in sequence. The second name change to
“Brad” will always be executed last.
Subscription operations
A subscription is a long-lived request that sends updates to the client when
new events happen.
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.
Arguments
Fields can have arguments, which have their type defined in the schema.
Arguments can be either optional or required. As discussed in the Schema
chapter, an argument is required if it is non-null (indicated with !) and there
is no default value (declared with =).
type 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
}
}
Fragments
Fragments allow us to reuse parts of the query. Fragments are a list of
selected fields (including sub-selections) for a certain type. Fragments have
a name and type condition, which must be an object, interface, or union.
query petOwners {
pets {
owner {
...personDetails
}
previousOwner {
...personDetails
}
}
}
In this example, we use personDetails twice, for both the owner and
previousOwner fields.
Inline fragments
Inline fragments are a selection of fields with a type condition. Inline
fragments are used to query different fields depending on the type. You can
think of them as switch statements, that depend on the type of the previous
field.
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.
interface Pet {
name: String
}
We can write inline fragments to query the doesBark field for Dog results
and doesMeow for Cat results.
query allThePets {
pets {
... on Dog {
doesBark
}
... on Cat {
doesMeow
}
}
}
Depending on the Pet type, we select different fields. We are only interested
in doesBark for Dogs, while for Cats we are only interested in doesMeow.
Types implementing interfaces will likely add additional fields which are
not shared across all implementations. For example, the doesBark field
only appears in the Dog type. Fragments or inline fragments must be used to
query fields which are not guaranteed across all implementations.
For example, consider this example schema with two important food
groups:
type Query {
dinner: [Food]
}
type Pizza {
name: String
toppings: [String]
}
type IceCream {
name: String
flavors: [String]
}
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.
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.
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.
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:
type Query {
search(filter: String!): String
}
query searches {
search1: search(filter: "Foo")
search2: search(filter: "Bar")
}
{
"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.
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.
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
}
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 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.
GraphQL Java needs to know how to load the data for every field,
therefore every field has an associated DataFetcher.
Before we go on, there are a few key classes which work closely with
DataFetchers. In GraphQL Java, a GraphQLSchema is both the structure (or
shape) of the API, and all the logic needed to execute requests against it.
Another key GraphQL Java concept is RuntimeWiring, which contains
GraphQLCodeRegistry, a map of schema fields to DataFetchers. Each
DataFetcher needs to be registered in the GraphQLCodeRegistry inside
RuntimeWiring.
type Query {
favoritePet: Pet
}
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
package myservice.service;
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.graphql.data.method.annotation
.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
record PetsController(PetService petService) {
@QueryMapping
Pet favoritePet() {
return petService.getFavoritePet();
}
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.
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.
package myservice.service;
package myservice.service;
@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.
type Query {
favoritePet: Pet
}
interface Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
Most likely, you will not need to write your own TypeResolvers, because
Spring for GraphQL registers a default ClassNameTypeResolver which
implements the TypeResolver interface. It tries to match the simple class
name of the value to a GraphQLObjectType. If it cannot find a match, it
will continue searching through super types, including base classes and
interfaces. This default TypeResolver is registered when
graphql.GraphQL is initialized.
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.
package myservice.service;
interface Pet {
String name();
String ownerId();
}
package myservice.service;
package myservice.service;
Then add two owner DataFetchers for Dog and Cat types.
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.graphql.data.method.annotation
.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
record PetsController(PetService petService) {
@QueryMapping
Pet favoritePet() {
return petService.getFavoritePet();
}
@SchemaMapping
Person owner(Dog dog) {
return petService.getPerson(dog.ownerId());
}
@SchemaMapping
Person owner(Cat cat) {
return petService.getPerson(cat.ownerId());
}
}
query bestPet {
favoritePet {
name
owner {
firstName
}
...on Dog {
doesBark
}
...on Cat {
doesMeow
}
}
}
import graphql.schema.TypeResolver;
import graphql.schema.idl.TypeRuntimeWiring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuratio
import org.springframework.graphql.execution
.RuntimeWiringConfigurer;
@Configuration
class Config {
g {
TypeResolver petTypeResolver = (env) -> {
// Your custom type resolver logic
};
@Bean
RuntimeWiringConfigurer runtimeWiringConfigurer() {
return wiringBuilder -> {
TypeRuntimeWiring petWiring = newTypeWiring("Pet")
.typeResolver(petTypeResolver)
.build();
wiringBuilder.type(petWiring);
};
}
With Spring for GraphQL, the arguments are declared with the @Argument
annotation.
package myservice.service;
@Controller
class SearchController {
@QueryMapping
String search(@Argument String pattern, @Argument int lim
// Your search logic here
}
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
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.
input SearchInput {
pattern: String
limit: Int
}
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
}
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) {
Map<String, Object> input = env.getArgument("input");
String pattern = (String) input.get("pattern");
int limit = (int) input.get("limit");
// Your search logic here
}
Argument Description
@Arguments Binding all arguments to a single object
“Source” Access to the source (parent) instance of t
field
DataLoader A DataLoader from the
DataLoaderRegistry. See the chapter
about DataLoader
@ContextValue A value from the main GraphQLContext
DataFetchingEnvironment. See more
context in the DataFetchers in depth chap
@LocalContextValue A value from the local GraphQLContext
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.getAuthenticatio
type Pet {
"String is a built-in scalar, therefore no declaration
name: String
dateOfBirth: Date
}
To use the Date scalar in the GraphQL Java Extended Scalars library, add
the package.
implementation 'com.graphql-java:graphql-java-extended-sca
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.
package myservice.service;
import graphql.scalars.ExtendedScalars;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuratio
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.
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 Excepti
}
The interface has only one method get with one argument
DataFetchingEnvironment. The returned result can be anything. This
interface directly reflects a core principle of GraphQL, it is agnostic about
where the data comes from.
Let’s implement DataFetchers for the simple Pet schema in the earlier
Spring for GraphQL example. Note that we are implementing the initial
example, where Pet is an object type and not an interface.
type Query {
favoritePet: Pet
}
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
The Pet record class returned by the PetService is the same as the class
used in the Spring for GraphQL example earlier in this chapter.
package myservice.service;
package myservice.service;
As Pet contains only a ownerId and not a full Person object, we need to
load more data. Let’s implement another DataFetcher.
// Lower level layer service does the work of retrieving da
PetService petService = new PetService();
@SchemaMapping
Person owner(Pet pet) {
return petService.getPerson(pet.ownerId());
}
Spring for GraphQL injects the Pet source object as a method parameter,
rather than having to manually access it from the
DataFetchingEnvironment with env.getSource().
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
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.
The source object is specific for every non-root DataFetcher. It gives each
child DataFetcher a source of data and additional information.
A GraphQLSchema is both the structure of the API, and all the logic needed
to execute requests against it.
type Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().p
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.
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:
type Query {
favoritePet: Pet
}
interface Pet {
name: String
owner: Person
}
type Person {
firstName: String
lastName: String
}
return null;
}
};
The best way to get a project up and running with Spring for GraphQL is
via the Spring Initializr tool at https://start.spring.io. We previously created
the application in the “Your first Spring for GraphQL service” section of the
Introduction chapter.
org.springframework.graphql:spring-graphql: Integrates
GraphQL Java with the Spring framework.
org.springframework.boot:spring-boot-starter-graphql: This
is the Spring Boot Starter for GraphQL.
org.springframework.graphql:spring-graphql-test: Testing
support for Spring for GraphQL. We’ll discuss this in the Testing chapter.
In addition to the Spring for GraphQL repo, some Boot-specific code is in
the Spring Boot repository.
GraphQL Java
GraphQL Java is automatically included with Spring for GraphQL. If you
are using GraphQL Java directly, it can be added to your project as a
dependency via Maven or Gradle.
Every version of GraphQL Java has two parts: the major and bug fix part.
At the time of writing, the latest Spring for GraphQL 1.1.2 uses GraphQL
Java 19.2. GraphQL Java doesn’t use semantic versioning.
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.
Configuration properties
Spring for GraphQL offers configuration properties to adjust the default
behavior without writing any code.
Path: By default, the GraphQL API is exposed via /graphql. This can be
modified by setting spring.graphql.path to another value.
Pet schema
Let’s continue with the GraphQL service we started in the “Your first
Spring for GraphQL service” section of the introduction chapter, an API for
pets. Review the Introduction chapter for instructions on how to create and
download a Spring for GraphQL service.
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
color: String
}
Controllers
Recall from the DataFetchers chapter that DataFetchers load data for
exactly one field. They are the most important concept in executing a
GraphQL request because they represent the logic that connects your
schema and your data.
We will use Spring for GraphQL’s annotation-based programming model to
register DataFetchers, which was previously discussed in the DataFetchers
chapter. @Controller components use annotations to declare handler
methods as DataFetchers for specific GraphQL fields. As we have a query
field pets, let’s use the shortcut @QueryMapping annotation. We’ll
represent Pets in Java as a record class.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class PetsController {
@QueryMapping
List<Pet> pets() {
return List.of(
new Pet("Luna", "cappuccino"),
new Pet("Skipper", "black"));
}
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.
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
}
@QueryMapping
Flux<Pet> pets() {
return petWebClient.get()
.uri("/pets")
.retrieve()
.bodyToFlux(Pet.class);
}
Source object
A core concept of GraphQL is that you can write flexible queries to retrieve
exactly the information you need.
Let’s expand our Pet schema to model one owner per pet.
type Query {
pets: [Pet]
}
type Pet {
name: String
color: String
owner: Person
}
type Person {
name: String
}
Now we can query the owner’s name for each pet.
query petsAndOwners {
pets {
name
owner {
name
}
}
}
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
this.ownerWebClient = builder.baseUrl("http://owner-se
.build();
}
@QueryMapping
Flux<Pet> pets() {
return petWebClient.get()
.uri("/pets")
.retrieve()
.bodyToFlux(Pet.class);
}
// New
@SchemaMapping
Mono<Person> owner(Pet pet) {
return ownerWebClient.get()
.uri("/owner/{id}", pet.ownerId())
.retrieve()
.bodyToMono(Person.class);
}
}
The owner DataFetcher returns the owner for exactly one pet. We use
bodyToMono to convert the JSON response to a Java object.
To account for the new Pet schema field owner, we added a new property
ownerId to the Pet class. This ownerId is used to construct the URL to
fetch owner information. Note that the Pet class contains an ownerId
which is not exposed, so a client cannot query it.
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 {
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
}
}
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 {
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service
this.ownerWebClient = builder.baseUrl("http://owner-se
.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.
type Query {
pets: [Pet]
pet(id: ID!): Pet
petSearch(input: PetSearchInput!): [Pet] # New field
}
type Pet {
id: ID!
name: String
color: String
owner: Person
}
type Person {
name: String
}
Let’s add a Java class PetSearchInput to represent the input type in the
schema. To access this input object as an argument, we add it as a parameter
to the petSearch method.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation
.Argument;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.graphql.data.method.annotation
.SchemaMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.reactive.function.client
.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Controller
class PetsController {
WebClient petWebClient;
WebClient ownerWebClient;
PetsController(WebClient.Builder builder) {
this.petWebClient = builder.baseUrl("http://pet-service
this.ownerWebClient = builder.baseUrl("http://owner-se
.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())
.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
}
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]
}
input PetSearchInput {
namePattern: String
ownerPattern: String
}
type Pet {
id: ID!
name: String
color: String
owner: Person
}
type Person {
name: String
}
The return type for the mutation field ends with Payload to follow a quasi-
standard naming convention for mutation response types.
This is a mutation request to change the name of the pet with the id “123”
to “Mixie”.
mutation changeName {
changePetName(id: "123", newName: "Mixie") {
pet {
name
}
}
}
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
this.ownerWebClient = builder.baseUrl("http://owner-se
.build();
.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);
}
@QueryMapping
Flux<Pet> petSearch(@Argument PetSearchInput input) {
// perform the search
}
// New
@MutationMapping
Mono<ChangePetNamePayload> changePetName(
@Argument String id,
@Argument String newName
) {
Map<String, String> changeNameBody = Map.of(
"name", newName
);
return petWebClient.put()
.uri("/pets/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(changeNameBody))
.retrieve()
.bodyToMono(ChangePetNamePayload.class);
}
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
}
# 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;
package myservice.service;
Let’s register a DataFetcher for the new creatures query field. Note that
the incoming data must be converted into the correct Java class
representation. This will depend on the implementation of the remote
service. For example, the service may return a field for each Creature to
indicate its type.
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"));
}
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
}
}
}
spring.graphql.graphiql.enabled=true
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.
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.
subscription myOrders {
newOrderCreated {
id
createdTime
customer {
name
}
}
}
{
"newOrderCreated": {
"id": "123",
"createdTime": "2015-07-06T04:11:11.000Z",
"customer": {
"name": "Andi"
}
}
}
{
"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.
Getting started
Let’s implement subscriptions with Spring for GraphQL.
Start with a new Spring for GraphQL project with Spring Initializr at
https://start.spring.io/, as we did in the “Your first Spring for GraphQL
service” section of the Introduction chapter. Choose Spring for GraphQL as
a dependency, and choose either Spring Web or Spring Reactive Web as a
dependency. In the following examples we will use Spring Reactive Web,
which includes Spring WebFlux. If you prefer to use subscriptions with
WebMVC instead of WebFlux, add
org.springframework.boot:spring-boot-starter-websocket as a
dependency, and adjust the examples by removing Mono and Flux.
type Subscription {
hello: String
}
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
d l El t (D ti fS d (1))
.delayElements(Duration.ofSeconds(1));
return interval.map(integer -> "Hello " + integer);
}
spring.graphql.websocket.path=/graphql
spring.graphql.graphiql.enabled=true
Let’s test our subscription. Start the service and open the GraphiQL
playground at http://localhost:8080/graphiql.
subscription myFirstSubscription {
hello
}
Subscription request
Then we will see the response changing every second, from “Hello 0” to
“Hello 1” to “Hello 2”, as shown in the screenshot “Hello 2 answer”.
Hello 2 answer
If you instead saw this “isTrusted” error message, this is a generic message.
{
"errors": [
{
"isTrusted": true
}
]
}
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 Streams initiative defines this interface to provide a standard for
asynchronous stream processing.
When using GraphQL Java with Spring, we use Flux from Project Reactor,
which implements Publisher. We used Flux in the example earlier in this
chapter.
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.
# Not valid
subscription tooManyRoots {
newCats {
name
}
newDogs {
name
}
}
subscription myOrders {
newOrderCreated {
id
createdTime
customer {
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-ws. 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.
spring.graphql.websocket.path=/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 repo contains a
list of recipes for different clients.
In this chapter we discussed GraphQL subscriptions. Later in the Testing
chapter, we’ll discuss how to test subscriptions.
Request and response
In this chapter, we will take a closer look at requests and responses for
GraphQL, including the HTTP protocol.
Transport protocols are handled at the Spring for GraphQL level. GraphQL
Java does not dictate any transport protocol.
Request
The most important elements of a GraphQL request are the query, operation
name, and variables. Every GraphQL request over HTTP is a POST
encoded as application/json, with the body being a JSON object:
{
"query": "<document>",
"variables": {
<variables>
},
"operationName": "<operationName>"
}
The 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.
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.
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>
}
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.
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:
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.
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.
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.
Intercepting requests
Spring for GraphQL provides WebGraphQlInterceptor to intercept
requests.
import org.springframework.graphql.server.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
import org springframework stereotype Component;
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"
return response;
});
}
}
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
class BetaFeaturesInterceptor implements WebGraphQlInterce
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request, Chain chain
) {
boolean betaFeatures = request
.getHeaders()
.containsKey("beta-features");
request.configureExecutionInput((executionInput, builde
executionInput
.getGraphQLContext()
.put("beta-features", betaFeatures);
return executionInput;
});
return chain.next(request);
}
import graphql.ExecutionResult;
import org.springframework.graphql.server.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
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.getExecu
Map<Object, Object> newExtensions = new HashMap<>(
if (executionResult.getExtensions() != null) {
newExtensions.putAll(executionResult.getExtension
}
// Replace value with your request ID mechanism
newExtensions.put("request_id", "YOUR_REQUEST_ID_H
In the next chapter, we’re going to build on these concepts and discuss
GraphQL errors.
GraphQL errors
What happens when things go wrong? How do we communicate errors
from our GraphQL service?
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 errors from the GraphQL spec are:
query invalid {
{
foo
}
}
{
"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.
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.
query missing {
doesNotExist
}
{
"errors": [
{
"message": "Validation error (FieldUndefined@[doesNo
Field 'doesNotExist' in type 'Query' is undefined",
"locations": [
{
"line": 2,
"column": 5
}
],
"extensions": {
"classification": "ValidationError"
}
}
}
]
}
The message communicates that the field doesNotExist does not exist in
the Query type. As this error can be linked to a location in the GraphQL
document, it is provided.
Note that there was no data key in the GraphQL response, because no
DataFetchers were invoked. Execution was terminated when the validation
error was detected.
Field errors
Field errors are raised during the execution of a field, resulting in a partial
response. In other words, an error raised during the execution of a
DataFetcher.
type Query {
favoritePet: Pet
}
type Pet {
id: ID
name: String
friends: [Pet]
}
import java.util.List;
@Controller
class PetsController {
@QueryMapping
Pet favoritePet() {
// Logic to return the user's favorite pet.
// Logic mocked with Luna the Dog.
return Pet.pets.get(0);
}
@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 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.
{
"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.
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.
Classification Description
InvalidSyntax Request error due to invalid GraphQL
syntax
ValidationError Request error due to invalid request
OperationNotSupported Request error if request attempts to
perform an operation not defined in the
schema
DataFetchingException Field error raised during data fetching
NullValueInNonNullableField Field error when a field defined as non-
null in the schema returns a null value
BAD_REQUEST
UNAUTHORIZED
FORBIDDEN
NOT_FOUND
INTERNAL_ERROR
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.
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 {
p p {
@Override
protected GraphQLError resolveToSingleError(Throwable e
DataFetchingEnvironment env) {
return GraphqlErrorBuilder.newError(env)
.errorType(ErrorType.INTERNAL_ERROR) // Error class
.message("My custom message") // Overrides the mess
.build();
}
}
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;
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));
GraphQL differs from a database schema or REST API that we might use to
fetch data for a query.
For example, just because the current database backing the API doesn’t
allow the User.name field to be null doesn’t automatically mean it should
also be non-nullable in the schema. The design needs to be justified
independent of the current implementation.
type 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.
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: UserName
}
type UserName {
legalName: String
preferredName: String
}
However, this schema change breaks all existing clients, who are using
name, such as this query.
query broken {
user(id: "123") {
name
}
}
The query would suddenly become invalid and always result in an error
because name is no longer a User string field, and now it’s a UserName
object that needs a sub-selection.
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
}
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 {
type Use {
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.
type Query {
user(id: ID!): User
}
type User {
id: ID!
userName: UserName
}
type UserName {
legalName: String
preferredName: String
}
You might also choose to retain a deprecated field for the foreseeable future
rather than removing the field.
Connected
A GraphQL API should resemble a connected or graph-like structure for
maximum client flexibility. The client should be able to “traverse” from one
piece of data to another related one, in a single request.
type 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.
# First
query myIssue {
issue {
ownerId
}
}
# returns "123"
# Second
query myUser {
userById(id: "123") {
name
}
}
type Issue {
description: String
owner: User
}
type User {
id: ID
name: String
}
Now the client can directly query the full User object for the issue in one
query.
query connected {
issue {
owner {
name
}
}
}
For example, you might consider reusing input objects like this search filter.
type Query {
searchPets(filter: SearchFilter): [Pet]
searchHumans(filter: SearchFilter): [Human]
}
input SearchFilter {
name: String
ageMin: Int
ageMax: Int
}
Reusing the input object is not a good idea because it couples the two fields
unnecessarily together. What happens if we would like to add a breed field
for pets? Now we have either a filter for humans that includes a breed, or
we need to deprecate fields and introduce new ones.
The same principle is true for output types. This example can seem
tempting especially for mutations.
type Mutation {
deleteUser(input: DeleteUserInput!): ChangeUserPayload
updateUser(input: UpdateUserInput!): ChangeUserPayload
}
type ChangeUserPayload {
user: User
}
This example has the same problem as the reused input objects. Once we
want to change the return type for just one mutation, we have a problem.
The other trap we might fall into is trying to combine multiple use cases
into one field. Fields are cheap, like any other element. Our service doesn’t
get slower, or have any other direct negative effects with a larger amount of
fields. We should make single-purpose fields explicit with specific naming.
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.
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
}
type B {
c: C!
}
type C {
d: String!
}
query myQuery {
a {
b {
c {
d
}
}
}
}
{
"data": {
"a" : null
}
}
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.
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
}
The most basic examples are id fields. Normally, if the fetching of the id is
unsuccessful, we can’t guarantee anything else and therefore we should
make it non-nullable.
type User {
id: ID!
name: UserName
address: Address
}
In this example, name and address are not as fundamental and therefore
not declared non-null.
Sometimes there are other fields that we should also make non-nullable. A
User could have a primary email to login in, but it is reasonable to assume
that this is such an important field that we don’t want to serve any data if
we can’t load the primary email.
type User {
id: ID!
primaryEmail: String! # also non-null
name: UserName
address: Address
}
type Order {
id: ID!
customer: Customer!
# And more order fields here
}
type Customer {
id: ID!
}
Perhaps this schema looks fine because we store all the current orders in
one database and if we can load an order, then we also load the customer at
the same time. But now imagine that we change our architecture in the
future and decide to introduce an order service and a separate customer
service. Suddenly we have a situation where we could load the order, but
not the customer, resulting in the whole Order being null when the error
propagates up.
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.
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.
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.
A simple list such as pets: [Pet] can quickly become too large for
clients to handle. A simple list restricts clients to only two options:
requesting all the data, or none at all. If requested, the list will be returned
in its entirety, regardless of the size.
Schema
This is an example Pet schema implementing the Relay connections
specification. We’ll go into further details of the specification after walking
through example queries.
type Query {
pets(first: Int, after: String, last: Int, before: Stri
PetConnection
}
}
type PetConnection {
edges: [PetEdge]
pageInfo: PageInfo!
}
type PetEdge {
cursor: String!
node: Pet!
}
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
type Pet {
name: String
# Your additional Pet fields here
}
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.
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.
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
}
}
}
Connection
As we saw in the previous query examples, first and last are of type
Int because they represent how many objects we want to request. after
and before are of type String because they are cursors that identify a
position within a list of elements.
type Query {
pets(
first: Int,
after: String,
last: Int,
before: String,
namePattern: String
): PetConnection
}
The <Entity>Connection type must have at least the two fields edges
and pageInfo.
type PetConnection {
edges: [PetEdge]
pageInfo: PageInfo!
}
Pagination metadata is always called PageInfo and shared across all edges.
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
PageInfo
type PageInfo {
startCursor: String
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
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
}
}
}
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.
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.
Example:
type Mutation {
makePayment(input: MakePaymentInput!): MakePaymentPaylo
}
type MakePaymentPayload {
payment: Payment
error: MakePaymentError
}
enum MakePaymentError {
CC_INVALID,
PAYMENT_SYSTEM_UNAVAILABLE
}
This brings the payment errors into the response type, which appears in the
data section of the GraphQL response. Note how these errors are no longer
in the errors section of the GraphQL response.
{
"data": {
"makePayment": {
"payment": null,
"error": "CC_INVALID"
}
}
}
type Query {
pet(id: ID!): PetLookup
}
type Pet {
# Your Pet fields here
}
type PetLookupError {
# Your PetLookupError fields here
}
We can then use inline fragments to handle the result and error cases.
query myPet {
pet(id: "123") {
... on Pet {
# Your Pet fields here
}
... on PetLookupError {
# Your PetLookupError fields here
}
}
}
Mutation format
The GraphQL community mostly uses a specific format for mutation, which
comes originally from Relay.
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.
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.
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.
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.
package myservice.service;
import org.springframework.graphql.server.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
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, builde
executionInput
.getGraphQLContext()
.put("userId", "123");
return executionInput;
});
return chain.next(request);
}
Local context
It’s also possible to set local context which only provides data to child
DataFetchers, rather than changing global context.
For example, we have the following schema for customers and their orders.
type Query {
order: Order
customerById(id: ID!): Customer
}
type Order {
id: ID
customer: Customer
}
type Customer {
id: ID
contact: Person
}
type Person {
name: String
}
query customerDetails {
customerById(id: "ID-1") {
contact {
name
}
}
}
or
query orderDetails {
order {
customer {
contact {
name
}
}
}
}
package myservice.service;
package myservice.service;
package myservice.service;
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.
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.newContex
.put("personForContact", personForContact)
.build();
();
// Return data and a new local context
return DataFetcherResult.<Order>newResult()
.data(order)
.localContext(localContext)
.build();
}
@SchemaMapping
Customer customer(Order order) {
return orderService.getCustomer(order);
@SchemaMapping
Person contact(Customer customer, DataFetchingEnvironmen
GraphQLContext localContext = env.getLocalContext();
if (localContext != null
&& localContext.get("personForContact") instanceof Pe
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.
Then the DataFetcher for Customer.contact can make use of the pre-
loaded Person in local context, if it is available. If the Person is not
available, a request to the Person service will be made.
We will discuss these three patterns and when to use them. As the next few
examples demonstrate DataFetcher patterns, we will show snippets rather
than a full Spring for GraphQL controller.
Non-reactive DataFetcher
DoSomeThing service;
DataFetcher<MyType> myField = (env) -> {
return service.doSomething();
};
In a reactive service, this is still a valid option if the work is only fast
computation work, meaning no I/O is involved. The exact definition of
“fast” is domain-specific, but as a rough guide, “fast” would be work that
takes less than one millisecond to complete.
If the DataFetcher involves blocking I/O, we can offload the blocking call
to another thread.
Although wrapping an I/O call does not make the whole service completely
reactive, it may still be worth doing as it doesn’t block GraphQL Java itself
and allows for parallel fetching of fields.
Reactive I/O
ReactiveClient client;
DataFetcher<CompletableFuture<Something> df = (env) -> {
return client.call();
};
This pattern should be used in a reactive service every time I/O is involved.
Reactive compute work requires a bit more effort compared to the previous
example, as it requires offloading the actual work onto another thread.
Reactive or not?
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.
or
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 Context 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.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlReque
Chain chain) {
return chain.next(request)
.contextWrite(Context.of("loggingPrefix", "123"));
}
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.
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.
type Query {
search: String @deprecated(reason: "Too slow, please u
searchFast: String
}
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.
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
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:
query myPets2 {
pets {
name
age @skip(if: true)
}
}
# same as:
query myPets3 {
pets {
name
age @include(if: false)
}
}
@deprecated
You can optionally provide a reason for deprecation, which will appear in
automatically generated documentation and tooling. The default reason is
“No longer supported”.
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 the screenshot “Deprecated documentation
in GraphiQL”.
@specifiedBy
scalar DateTime
And this is still a valid way to define custom scalars in a schema. However,
only a name in the schema is not enough to explain the behaviour of custom
scalars. For example, DateTime implementations can vary across services,
but they might both contain a schema element with the same name
DateTime. The @specifiedBy directive was introduced later to provide a
way to clearly document the behavior of custom scalars. The provided URL
should link to a specification including data format, serialization, and
coercion rules. For the full details and specification templates, see the
GraphQL Scalars project.
With the GraphQL Scalars project, you can create your own custom scalars
specifications and host them on the GraphQL Foundation’s
scalars.graphql.org domain, like the linked URL in the previous
example. You can also read and link to other contributed specifications. See
the GraphQL Scalars project for more information.
To make this a valid schema, we could add another location to the directive
definition. Provide multiple locations by separating them with |.
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_D
type Query {
pet: Pet
}
type Pet {
name: String
lastTimeOutside: String
}
Directives can also have arguments. Let’s add a maxAge argument, with a
default value of 1000.
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
}
Repeatable directives
We can define schema and operation directives as repeatable, enabling it
to be used multiple times in the same location. If repeatable is not
included in the directive definition, the directive will be non-repeatable by
default.
type Query {
# Multiple owners per field possible
hello: String @owner(name: "Brian") @owner(name: "Josh
}
As this is an advanced chapter, the code examples which follow are more
complicated and involve schema traversal and transformation.
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.
In pure GraphQL Java, usage of our schema directive can be accessed via
our instance of GraphQLSchema called schema:
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.getFieldDe
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
type Query {
hired(applications : [Application!] @size(min : 3)) :
}
implementation 'com.graphql-java:graphql-java-extended-vali
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 SchemaDirecti
ValidationSchemaWiring validationDirectiveWiring
= new ValidationSchemaWiring(possibleRules);
return wiringBuilder -> wiringBuilder
.directiveWiring(validationDirectiveWiring
}
}
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
With pure GraphQL Java, we can make use of schema transformer and type
visitor tools.
@Override
public TraversalControl visitGraphQLFieldDefinition(
GraphQLFieldDefinition fieldDefinition,
TraverserContext<GraphQLSchemaElement> context
p Q
) {
GraphQLSchemaElement parentNode = context.getParentNode
if (!(parentNode instanceof GraphQLObjectType)) {
return TraversalControl.CONTINUE;
}
GraphQLObjectType objectType = (GraphQLObjectType) pare
GraphQLAppliedDirective directive = objectType
.getAppliedDirective("suffix");
if (directive != null) {
String suffix = directive.getArgument("name").getValu
GraphQLFieldDefinition newFieldDefinition
= fieldDefinition.transform(builder
-> builder.name(fieldDefinition.getName() + suffi
return changeNode(context, newFieldDefinition);
}
return TraversalControl.CONTINUE;
}
});
We visit every field definition and try to get the object containing the field
via context.getParentNode(). Then we get the
GraphQLAppliedDirective for the suffix. We use this to create a
GraphQLFieldDefinition with the changed name. The last thing to do is
to call changeNode (from GraphQLTypeVisitor) which actually changes
the field.
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 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.
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)
}
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
po t o g sp g a e o g ap q data et od a otat o
.QueryMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
class GreetingController {
@QueryMapping
String hello(DataFetchingEnvironment env) {
QueryDirectives queryDirectives = env.getQueryDirective
List<QueryAppliedDirective> cacheDirectives = queryDire
.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();
package myservice.service;
import org.springframework.boot.autoconfigure.graphql
.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
i t i f k t t t ti C fi ti
import org.springframework.context.annotation.Configuratio
@Configuration
class GraphQlConfig {
@Bean
GraphQlSourceBuilderCustomizer sourceBuilderCustomizer
return (builder) ->
builder.configureGraphQl(graphQlBuilder ->
// Here we can use `GraphQL.Builder`
// For example, executionIdProvider
graphQlBuilder
.executionIdProvider(new MyExecutionIdP
}
}
If you are using GraphQL Java without Spring for GraphQL, this is how to
manually initialize the graphql.GraphQL object.
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.
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.
These are the steps between the GraphQL Java engine receiving a
ExecutionInput request and returning a ExecutionResult instance with
data and/or errors.
The first step is parsing the “query” (document) value from the
ExecutionInput and validating it. If the document contains invalid
syntax, the parsing fails immediately. Otherwise, if the document is
syntactically valid, it then is validated against the schema.
Coercing variables
See the query language chapter for an overview of GraphQL variables and
see how variables are sent in an HTTP request in the request and response
chapter.
This query has one variable $name with the type String. If the request now
contains the following variables, variable coercing would fail since we
expect a single String for name, not a list of Strings.
{
"name": ["Luna", "Skipper"]
}
Fetching data
The last step is the core of execution: GraphQL Java fetching the data
needed to fulfill the request.
Let’s look more closely at an example schema and query, which will help us
understand the overall execution algorithm.
type Query {
dogs: [Dog]
}
type Dog {
name: String
owner: Person
friends: [Dog]
details: DogDetails
}
type Person {
firstName: String
lastName: String
}
type DogDetails {
barking: Boolean
shedding: Boolean
}
query myDogs {
dogs {
name
owner {
firstName
lastName
}
friends {
name
}
details {
barking
shedding
}
}
}
Tree of Fields
This query, as shown in the diagram “Tree of Fields”, has three levels, with
dogs as the single root field. GraphQL Java traverses the query breadth-
first and invokes the corresponding 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.
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 the
diagram “Execution Order”.
Execution Order
In this diagram, the numbers show the order of execution. Fields with the
same number are executed in parallel.
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.
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.
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.
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
}
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.
type Query {
pet: Pet
}
interface Pet {
name: String
}
query myPet {
pet {
...on Dog {
barks
}
...on Cat {
meows
}
}
}
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.
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!
}
}
}
}
}
package myservice.service;
import graphql.analysis.MaxQueryDepthInstrumentation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuratio
@Configuration
class MyGraphQLConfiguration {
@Bean
MaxQueryDepthInstrumentation maxQueryDepthInstrumentati
return new MaxQueryDepthInstrumentation(15);
}
}
{
"errors": [
{
"message": "maximum query depth exceeded 42 > 15",
"extensions": {
"classification": "ExecutionAborted"
}
}
]
}
import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationCo
import graphql.execution.instrumentation.InstrumentationSta
import graphql.execution.instrumentation.SimpleInstrumenta
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> beginExecu
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, Throw
System.out.println("execution time: "
+ (System.currentTimeMillis() - timeStart.get())
}
};
}
}
At the time of writing, the latest Spring for GraphQL 1.1 uses GraphQL
Java 19.x. In Spring for GraphQL 1.2, GraphQL Java 20.x will be used,
which adds the improved SimplePerformantInstrumentation class. It
is designed to be more performant and reduce object allocations.
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.
/**
* This is invoked when the instrumentation step is ini
* dispatched
*
* @param result the result of the step as a completab
*/
void onDispatched(CompletableFuture<T> result);
/**
* This is invoked when the instrumentation step is fu
*
* @param result the result of the step (which may be
* @param t this exception will be non-null if an
* was thrown during the step
*/
void onCompleted(T result, Throwable t);
InstrumentationState
Let’s discuss how state is managed in instrumentation.
import graphql.ExecutionResult;
import graphql.execution.instrumentation.InstrumentationCo
import graphql.execution.instrumentation.InstrumentationSta
import graphql.execution.instrumentation.SimpleInstrumenta
p g p q p
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;
@Component
class FieldCountInstrumentation
extends SimpleInstrumentation {
@Override
public InstrumentationState createState(
InstrumentationCreateStateParameters parameters) {
return new FieldCountState();
}
@Override
public InstrumentationContext<ExecutionResult> beginField
InstrumentationFieldParameters parameters,
InstrumentationState state) {
((FieldCountState) state).counter.incrementAndGet();
return noOp();
}
@Override
public InstrumentationContext<ExecutionResult> beginExecu
InstrumentationExecutionParameters parameters
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
return new InstrumentationContext<ExecutionResult>() {
@Override
public void onDispatched(
CompletableFuture<ExecutionResult> result) {
}
@Override
public void onCompleted(ExecutionResult result, Throw
System.out.println(
"finished with " +
((FieldCountState) state).counter.get() +
" Fields called"
);
}
};
}
}
ChainedInstrumentation
Spring for GraphQL automatically chains all detected instrumentation
beans. No further configuration is required.
ChainedInstrumentation chainedInstrumentation
= new ChainedInstrumentation(chainedList);
Built-in instrumentations
For convenience, GraphQL Java contains built-in instrumentations.
Name
DataLoaderDispatcher For DataLoader.
Instrumentation
ExecutorInstrumentation Controls on which thread calls to
DataFetchers happen on
FieldValidationInstrumentation Validates fields and their arguments
before query execution. If errors are
returned, execution is aborted.
MaxQueryComplexity Prevents execution of very complex
Instrumentation operations.
MaxQueryDepthInstrumentation Prevents execution of very large
operations.
Name
TracingInstrumentation Implements the Apollo Tracing
format.
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
AndVariables and/or the variables
instrumentSchema Allows for changing the GraphQLSchema
Step Description
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
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
}
Let’s register two DataFetchers responsible for loading people and then
their bestFriend in Spring for GraphQL.
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.graphql.data.method.annotation
.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
record PersonController(PersonService personService) {
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
@SchemaMapping
Person bestFriend(Person person) {
return personService.getPersonById(person.bestFriendId
}
}
While this code works, it will not perform well with large lists. For every
person in the list, we invoke the DataFetcher for the best friend. For “n”
people, we now have “n+1” service calls: one for loading the initial list of
people and then one for each of the n people to load their best friend. This is
where the name “n+1 problem” comes from. This can cause significant
performance problems as large lists will require many calls to retrieve data.
The 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-
dataloader, which is maintained by the GraphQL Java team. This library is
a port of the JS library DataLoader. 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.
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();
}
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.
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.
// Setup
UserService userService = ...;
// expected to return a CompletableFuture
BatchLoader<Integer, User> userBatchLoader = userIds ->
userService.loadUsersById(userIds);
// Usage
CompletableFuture<User> user1CF = userLoader.load(1);
CompletableFuture<User> user2CF = userLoader.load(2);
userLoader.dispatchAndJoin();
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.
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.newRuntimeWi
import static graphql.schema.idl.TypeRuntimeWiring.newTypeW
import static java.util.concurrent.CompletableFuture.comple
type Person {
name: String
bestFriend: Person
}
""";
TypeDefinitionRegistry parsedSdl = new SchemaParser().p
DataFetcher<CompletableFuture<Person>> bestFriendDF =
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:
// Execute query
GraphQL graphQL = GraphQL.newGraphQL(schema).build();
ExecutionResult executionResult = graphQL.execute(exec
}
}
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.
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;
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
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;
@QueryMapping
List<Person> people() {
return personService.getAllPeople();
}
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 implements equals and
hashcode methods in order to work as a key.
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.
Argument Description
List<T> The list of source objects
java.security.Principal Spring Security principal
@ContextValue(name = A specific value from the GraphQLContex
@ ( p p Q
“foo”) Argument Description
GraphQLContext The entire GraphQLContext
BatchLoaderEnvironment org.dataloader.BatchLoaderWithCon
from DataLoader itself
In this chapter, we covered the n+1 problem when too many service calls
are used to fetch data. We solved the problem with DataLoader, which is
conveniently made available with @BatchMapping in Spring for GraphQL.
We then had a closer look at how DataLoader works under the hood.
Testing
Spring for GraphQL provides helpers for GraphQL testing in a dedicated
artifact org.springframework.graphql:spring-graphql-test.
Testing a GraphQL service can happen on multiple levels, with different
scopes. In this chapter, we will discuss how Spring for GraphQL makes it
easier to write tests. At the end of the chapter, we’ll conclude with our
recommendations for writing good tests.
In this chapter, we will make use of standard testing libraries. We will use
JUnit 5, Mockito, AssertJ, and the standard Spring Boot testing capabilities.
type Query {
hello: String
}
A simple query:
query greeting {
hello
}
package myservice.service;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
class GreetingController {
@QueryMapping
String hello() {
return "Hello, world!";
}
}
package myservice.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org mockito junit jupiter MockitoExtension;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class GreetingControllerTest {
@Test
void testHelloDataFetcher() {
GreetingController greetingController = new GreetingCon
GraphQlTester
GraphQlTester is the primary class to help us test in Spring for GraphQL.
GraphQlTester is a Java interface with a few inner interfaces, which
provides a rich API to execute requests and verify responses. There are a
number of implementations for different types of tests:
For example, here is a query for our Hello World example from earlier in
this chapter:
query greeting {
hello
}
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.context.SpringBootTes
import org.springframework.graphql.test.tester.HttpGraphQlT
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_POR
class GreetingControllerTest {
@Autowired
HttpGraphQlTester graphQlTester;
@Test
void usingTester() {
graphQlTester
.document("query greeting { hello }")
.execute()
.path("hello")
.entity(String.class)
.isEqualTo("Hello, world!");
}
}
We’ll soon explain all the parts in this test, but let’s start by focusing on the
GraphQlTester. We provide a document with document, execute it,
select a specific part of the response to verify with path, and finally verify
it is the string “Hello, world!”.
To make testing code more compact, note that the document in this example
is provided on a single line. This is equivalent to a query with new lines,
because new lines and additional whitespace are ignored in GraphQL
syntax.
We’ll see how this GraphQlTester fits into a test class in multiple
examples later in this chapter.
document or documentName
Then we could rewrite our earlier test with documentName to use this
resource file containing the document:
graphQlTester
.documentName("greeting")
.execute()
.path("hello")
.entity(String.class)
.isEqualTo("Hello, world!");
interface Traversable {
Path path(String path);
}
We can use any JsonPath 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.
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
}
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:
Pet favoritePet = graphQlTester
.document("query whoIsAGoodBoyOrGirl { favoritePet
.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!");
Note that the next few sections focus on HTTP where tests include
transport. For WebSocket tests, see subscriptions testing section later in this
chapter.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_P
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.context.SpringBootTes
import org.springframework.graphql.test.tester.HttpGraphQlT
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_P
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).
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.autoconfigure.graphql
.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTes
import org.springframework.graphql.test.tester.HttpGraphQlT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironm
@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. Create a new tester
instance by providing the relevant WebGraphQlHandler.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.context.SpringBootTes
import org springframework graphql server WebGraphQlHandle
import org.springframework.graphql.server.WebGraphQlHandle
import org.springframework.graphql.test.tester.WebGraphQlTe
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironm
class WebGraphQlTest {
@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.Autowi
import org.springframework.boot.test.context.SpringBootTes
import org.springframework.graphql.ExecutionGraphQlService
import org.springframework.graphql.test.tester
.ExecutionGraphQlServiceTester;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironm
class GraphQlServiceTest {
@Autowired
ExecutionGraphQlService graphQlService;
@Test
void testHello() {
ExecutionGraphQlServiceTester graphQlServiceTester
= ExecutionGraphQlServiceTester.create(graphQlS
String document = "query greeting { hello }";
graphQlServiceTester.document(document)
.execute()
.path("hello")
.entity(String.class)
.isEqualTo("Hello, world!");
}
type Query {
pets: [Pet]
}
type Pet {
name: String
}
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;
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
}
Now with @GraphQlTest, our test setup includes the PetController, but
not the PetService because it doesn’t belong to the GraphQL layer itself.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.autoconfigure.graphql
.GraphQlTest;
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.graphql.test.tester.GraphQlTeste
import java.util.List;
@GraphQlTest(PetsController.class)
class PetsControllerTest {
@Autowired
GraphQlTester graphQlTester;
@MockBean
PetService petService;
PetService petService;
@Test
void testPets() {
Mockito.when(petService.getPets())
.thenReturn(List.of(
new Pet("Luna"),
new Pet("Skipper")
));
graphQlTester
.document("query myPets { pets { name } }")
.execute()
.path("pets[*].name")
.entityList(String.class)
.isEqualTo(List.of("Luna", "Skipper"));
}
As an alternative, you could verify there were at least two pet names by
replacing the last block of the test above with:
graphQlTester
.document("query myPets { pets { name } }")
.execute()
.path("pets[*].name")
.entityList(String.class)
.hasSizeGreaterThan(2);
In these examples, the path for Pets was more complex than our Hello
World example. "pets[*].name" means select all names of all pets. We
can use any JsonPath with path.
Subscription testing
GraphQlTester offers an executeSubscription method that returns a
GraphQlTester.Subscription. This can be then further converted to a
Flux and verified. To test Flux more easily, add the Reactor testing library
io.projectreactor:reactor-test.
Testing our hello subscription from the Subscription chapter end to end
looks like this.
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
.delayElements(Duration.ofSeconds(1));
return interval.map(integer -> "Hello " + integer);
}
}
}
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.SpringBootTes
import org.springframework.graphql.test.tester.GraphQlTeste
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_P
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();
).build();
}
@Test
void helloSubscription() {
Flux<String> hello = graphQlTester
.document("subscription mySubscription {hello}")
.executeSubscription()
.toFlux("hello", String.class);
StepVerifier.create(hello)
.expectNext("Hello 0")
.expectNext("Hello 1")
.expectNext("Hello 2")
.verifyComplete();
}
}
The same way we have tested subscription end to end here also allows us to
test subscriptions on different layers.
Testing recommendations
A general guide for writing good tests is to have the smallest or most
focused test possible that verifies what we want to test.
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.
These testing guidelines fit into the Test Pyramid model. The idea is to have
more of focused, smaller and faster tests, compared to the number of tests
that run longer, test more aspects, and are harder to debug. This model gives
us some guidance about the amount of tests per test type. It is preferable to
have more unit tests than WebGraphQlHandlerTests, and it is preferable
to have more WebGraphQlHandlerTests than the number of end-to-end
tests.
As security is not part of the GraphQL Java engine, this chapter will instead
focus on using Spring for GraphQL and Spring Security to secure your
GraphQL service. Spring for GraphQL has built-in, dedicated support for
Spring Security.
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.
type Order {
id: ID
details: String
}
type Mutation {
# Only Admins can delete orders
deleteOrder(input: DeleteOrderInput!): DeleteOrderPaylo
}
input DeleteOrderInput {
orderId: ID
}
type DeleteOrderPayload {
success: Boolean
}
Let’s implement a very simple Java class OrderService that loads and
changes orders, which are stored in memory.
package myservice.service;
package myservice.service;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
class OrderService {
OrderService() {
// A mutable list of orders
this.orders = new ArrayList<>(List.of(
new Order("1", "Kibbles", "Luna"),
new Order("2", "Chicken", "Skipper"),
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")
));
}
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.
org.springframework.boot:spring-boot-starter-security
package myservice.service;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuratio
import org.springframework.security.config.annotation.web
.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server
.ServerHttpSecurity;
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.authenticati
.RedirectServerAuthenticationSuccessHandler;
@Configuration
@EnableWebFluxSecurity
class Config {
@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSec
throws Exception {
http.formLogin().authenticationSuccessHandler(
new RedirectServerAuthenticationSuccessHandler("/gra
);
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> {
exchanges.anyExchange().authenticated();
})
.build();
}
@Bean
@SuppressWarnings("deprecation")
MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPassword
UserDetails luna = userBuilder
.username("Luna").password("password").roles("USER")
.build();
UserDetails andi = userBuilder
.username("Andi").password("password").roles("USER",
.build();
return new MapReactiveUserDetailsService(luna, andi);
}
}
Let’s create the last required class, the OrderController that implements
our DataFetcher.
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.
package myservice.service;
package myservice.service;
package myservice.service;
import org.springframework.graphql.data.method.annotation
.Argument;
import org.springframework.graphql.data.method.annotation
.MutationMapping;
import org.springframework.graphql.data.method.annotation
.QueryMapping;
import org.springframework.security.access
.AccessDeniedException;
import org.springframework.security.authentication
.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority
.SimpleGrantedAuthority;
import org.springframework.stereotype.Controller;
import java.security.Principal;
import java.util.List;
@Controller
record OrderController(OrderService orderService) {
@QueryMapping
List<Order> myOrders(Principal principal) {
return orderService.getOrdersByOwner(principal.getName
}
@MutationMapping
DeleteOrderPayload deleteOrder(@Argument DeleteOrderInpu
Principal principa
UsernamePasswordAuthenticationToken user
= (UsernamePasswordAuthenticationToken) principal;
if (!user.getAuthorities()
.contains(new SimpleGrantedAuthority("ROLE_ADMIN")))
throw new AccessDeniedException("Only admins can dele
}
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.
query lunaOrders {
myOrders {
id
}
}
Luna’s orders
mutation lunaUnauthorized {
deleteOrder(input: {orderId: 1}) {
success
}
}
Luna is unauthorized to delete orders
After we log out via /logout and login as “Andi” (with password
“password”), we can delete an order.
Method security
One problem with our store order example above is the location where we
perform the authorization checks. They happen directly inside each
DataFetcher. This is not great. The better and recommended way is to
secure the OrderService itself, so that it is secure, regardless which
DataFetcher uses it.
@PreAuthorize("hasRole('ADMIN')")
Mono<Boolean> deleteOrder(String orderId) {
return Mono.just(orders.removeIf(order -> order.id().equa
}
Mono<List<Order>> getOrdersForCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> {
Principal principal = securityContext.getAuthenticati
return orders
.stream()
fil ( d d () l ( i i l
.filter(order -> order.owner().equals(principal.ge
.collect(Collectors.toList());
});
}
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.Configuratio
import org.springframework.security.config.annotation.metho
.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;
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.authenticati
.RedirectServerAuthenticationSuccessHandler;
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class Config {
@Bean
@Bean
SecurityWebFilterChain springWebFilterChain(
ServerHttpSecurity http
) throws Exception {
http
.formLogin()
.authenticationSuccessHandler(
new RedirectServerAuthenticationSuccessHandler("/g
);
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> {
exchanges.anyExchange().authenticated();
})
.build();
}
@Bean
@SuppressWarnings("deprecation")
MapReactiveUserDetailsService userDetailsService() {
User.UserBuilder userBuilder = User.withDefaultPassword
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);
}
}
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.PreAutho
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
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.getAuthentica
return orders
.stream()
.filter(order -> order.owner().equals(principal.g
.collect(Collectors.toList());
});
}
@PreAuthorize("hasRole('ADMIN')")
Mono<Boolean> deleteOrder(String orderId) {
return Mono.just(orders
.removeIf(order -> order.id().equals(orderId)));
}
Testing auth
For an introduction to testing, please see the previous chapter on Testing.
Let’s write end-to-end auth tests. To start with the simplest test and
establish a good baseline, let’s verify that we reject unauthenticated
requests.
package myservice.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowi
import org.springframework.boot.test.autoconfigure.web.reac
.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTes
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_P
@AutoConfigureWebTestClient
class AuthE2ETest {
@Autowired
WebTestClient webTestClient;
@Test
void shouldRejectUnauthenticated() {
String document = "query orders { myOrders { id } }";
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);
}
}
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.Autowi
import org.springframework.boot.test.context.SpringBootTes
import org.springframework.context.annotation.Import;
import org.springframework.graphql.server.WebGraphQlHandle
import org.springframework.graphql.server.WebGraphQlInterce
import org.springframework.graphql.server.WebGraphQlReques
import org.springframework.graphql.server.WebGraphQlRespon
import org.springframework.graphql.test.tester.WebGraphQlTe
import org.springframework.security.authentication
.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority
SimpleGrantedAuthority;
.SimpleGrantedAuthority;
import org.springframework.security.core.context
.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityCo
import org.springframework.security.core.context
.SecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.List;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironm
@Import(WebGraphQlTest.WebInterceptor.class)
class WebGraphQlTest {
@Autowired
WebGraphQlHandler webGraphQlHandler;
@Component
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlReq
Chain chain)
UsernamePasswordAuthenticationToken authenticated =
UsernamePasswordAuthenticationToken.authenticated(
"Luna", "password",
List.of(new SimpleGrantedAuthority("ROLE_USER")));
Within the same class, we can also test that an unauthorized user cannot
delete orders.
@Test
void testMutationForbidden() {
WebGraphQlTester webGraphQlTester
= WebGraphQlTester.create(webGraphQlHandler);
String document = """
mutation delete($id:ID){
deleteOrder(input:{orderId:$id}){success}}""";
webGraphQlTester.document(document)
.variable("id", "1")
.execute()
.errors()
.expect(responseError ->
responseError.getMessage().equals("Forbidden") &&
responseError getPath() equals("deleteOrder")) ve
responseError.getPath().equals( deleteOrder )).ve
}
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.
Java client
Spring for GraphQL comes with a client, GraphQlClient, for making
GraphQL requests over HTTP or WebSocket.
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.HttpGraphQlClien
import org.springframework.web.reactive.function.client
.WebClient;
or
Once created, we can’t change any of these client settings. For different
settings, we need to mutate the client and use the builder methods again.
WebSocket client
The WebSocketGraphQlClient uses a WebSocketClient under the
hood. We have to provide a WebSocketClient when creating a new
WebSocketGraphQlClient. Note that WebSocketClient is an
abstraction with implementations for Reactor Netty, Tomcat and others.
Here is an example using a Netty-based implementation.
import org.springframework.graphql.client.WebSocketGraphQlC
import org.springframework.web.reactive.socket.client
.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client
.WebSocketClient;
Note that in the WebSocket client we provide the URL via the builder
builder(url, client), whereas in the HTTP client it is set via the
builder url.
Once created, we can’t change any WebSocket client settings. For different
settings, we need to mutate the client and use the builder methods again.
GraphQlClient
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.