Execution

After being validated, a GraphQL query is executed by a GraphQL server which returns a result that mirrors the shape of the requested query, typically as JSON.

GraphQL cannot execute a query without a type system, let’s use an example type system to illustrate executing a query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Query {
human(id: ID!): Human
}

type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}

enum Episode {
NEWHOPE
EMPIRE
JEDI
}

type Starship {
name: String
}

In order to describe what happens when a query is executed, let’s use an example to walk through.

1
2
3
4
5
6
7
8
9
{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}

You can think of each field in a GraphQL query as a function or method of the previous type which returns the next type.

Exactly this is how GraphQL works. Each field on each type is backed by a function called the resolver which is provided by the GraphQL Server Developer.

When a field is executed, the corresponding resolver is called to produce the next value.

If a field produces a scalar value like a string or number, then the execution completes.

However if a field produces an object value then the query will contain another selection of fields which apply to that object. This continues until scalar values are reached.

GraphQL queries always end at scalar values.

Root Fields & Resolvers

At the top level of every GraphQL server is a type that represents all of the possible entry points into the GraphQL API, it’s often called the Root Type or the Query Type.

In this example, our Query Type provides a field called human which accepts the argument id. The resolver function for this field accesses a database and then constructs and returns a Human object.

1
2
3
4
5
6
7
Query: {
human(obj, args, context) {
return context.db.loadHumanById(args.id).then(
userData => new Human(userData)
)
}
}
  • obj The previous object, which for a field on the root Query type if often not used.

  • args The arguments provided to the field in the GraphQL query

  • context A value which is provided to every resolver and holds important contextual information like the currently logged in user, or access to a database.

Asynchronous Resolvers

1
2
3
4
5
human(obj, arg, context) {
return context.db.loadHumanById(args.id).then(
userData => new Human(userData)
)
}

The context is used to provide access to a database which is used to load data for user by the id provided as an argument in the GraphQL query. Since loading from a database is an asynchronous operation, this returns a Promise. When the database returns, we can construct and return a new Human object.

Notice that while the resolver function needs to be aware of Promise, the GraphQL query does not. It simply expects the human field to return something which it can then ask the name of. During execution, GraphQL will wait for Promise to complete before continuing and will do so with optimal concurrency.

Trivial Resolvers

Now that a Human object is available, GraphQL execution can continue with the fields requested on it.

1
2
3
4
5
Human: {
name(obj, args, context) {
return obj.name
}
}

A GraphQL server is powered by a type system which is used to determine what to do next. Even before the human field returns anything, GraphQL knows that the next stop will be to resolve fields on the Human type since the type system tells it that the human field will return a Human.

Resolving the name in this case is very straight-forward. The name resolver function is called and the obj argument is the new Human object return from the previous field. In this case, we expect that Human object to have a nameproperty which we can read and return directly.

Scalar Coercion

While the name field is being resolved, the appearsIn and starships fields can be resolved concurrently. The appearsIn field could also have a trivial resolver, but let’s take a closer look:

1
2
3
4
5
Human: {
appearsIn(obj) {
return obj.appearsIn // return [3, 4, 5]
}
}

Notice that our type system claims appearsIn will return Enum values with known values, however this function is returning numbers. This is an example of scalar coercion. The type system knows what to expect and will convert the values returned by a resolver function into something that upholds the API contract. In this case, there may be an Enum defined on our server which uses numbers like 3, 4, 5 internally, but represents them as Enum values in the GraphQL type system.

List Resolvers

The appearsIn field above returned a list of Enum values, and since that’s what the type system expected, each item in the list was coerced to the appropriate enum values.

What happens when the starships field is resolved?

1
2
3
4
5
6
7
8
9
Human: {
starships (obj, args, context) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}

The resolver for this field is not just returning a Promise, it’s returning a list of Promises. The Human object has a list of ids of the Starships they piloted, but we need to go load all of those ids to get read Starship objects.

GraphQL will wait for all of these Promises concurrently before continuing, and when left with a list of objects, it will concurrently continue yet again to load the name field on each of these items.

Introspection

We designed the type system, so we know what types are available, but if we didn’t, we can ask GraphQL by querying the __schema field, always available on the root type of a Query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
{
__schema {
types {
name
}
}
}
// produce

{
"data": {
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "Episode"
},
{
"name": "Character"
},
{
"name": "ID"
},
{
"name": "String"
},
{
"name": "Int"
},
{
"name": "FriendsConnection"
},
{
"name": "FriendsEdge"
},
{
"name": "PageInfo"
},
{
"name": "Boolean"
},
{
"name": "Review"
},
{
"name": "SearchResult"
},
{
"name": "Human"
},
{
"name": "LengthUnit"
},
{
"name": "Float"
},
{
"name": "Starship"
},
{
"name": "Droid"
},
{
"name": "Mutation"
},
{
"name": "ReviewInput"
},
{
"name": "__Schema"
},
{
"name": "__Type"
},
{
"name": "__TypeKind"
},
{
"name": "__Field"
},
{
"name": "__InputValue"
},
{
"name": "__EnumValue"
},
{
"name": "__Directive"
},
{
"name": "__DirectiveLocation"
}
]
}
}
}

Let’s group them:

  • Query, Character, Human, Episode, Droid. These are the ones that we defined in our type system.

  • String, Boolean. These are built-in scalars that the type system provided.

  • __Schema, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive. These are preceded with a double underscore, indicating that they are part of the introspection system.