Using Resolvers
Resolvers are functions that make up the basis for how data is determined in resolving queries in GraphQL APIs. There
are two types of resolvers, Field Resolvers and Type Resolvers. Field Resolvers represent the specific data
returned within the data
key a result payload. Type Resolvers are responsible for taking an Abstract Type (GraphQL
Union or Interface types) and returning a string representing the concrete type.
Field Resolvers
Field resolvers are represented by the following function:
async function (parent, args, context, info) {
return data;
}
The majority of the time when Resolvers are mentioned in a GraphQL context it is referring to Field Resolvers. Field
Resolvers are responsible for returning the data
specified by the GraphQL Schema (or a Promise
of the data). Check
out the apollo documentation on resolvers, too, it
applies here and to GraphQL in general.
With the given schema:
schema {
# denotes the "Root Query Type" as the "Query" type
query: Query
}
# "Query" is the "Root Query Type" as denoted by the schema definition
type Query {
# root field that returns an "Actor"
actor: Actor!
}
type Actor {
name: String!
}
This schema would have two resolvers:
- One "root field resolver" on the
Query
type for theactor
field - One "non-root field resolver" on the
Actor
type for thename
field
Root (Query and Mutation) Field Resolvers
All GraphQL queries start with a "root" field. Root Field Resolvers are either on the root Query
type or the root
Mutation
type depending on whether it's a query or mutation operation, respectively. These root resolvers "kick off"
the query and being at the "root" typically have a null
parent argument (unless one has been explicitly declared).
A root resolver function for the actor
field could be:
async function (parent, args, context, info) {
return {
name: "Meryl Streep"
};
}
Non-Root Field Resolvers
Any type is that not the root Query type or the root Mutation type will be a type that contains non-root field
resolvers. In this case they are connected to the graph through some reference to the root Query and Mutation types.
Due to the graph-like nature of resolving queries these non-root fields and their resolvers can be referenced multiple
times and as such often rely on the parent
parameters to understand their connection in the graph.
The non-root field on Actor
is name
. When any type resolves an Actor
type each of the field resolvers receives
the parent and is responsible for returning a result for their field.
The name
field resolver on the Actor
type could look like:
async function (parent, args, context, info) {
return parent.name;
}
This example looks for the name
property on the parent. In this case the parent
was:
{
"name": "Meryl Streep"
}
The returned value is "Meryl Streep"
which matches the return value of String!
set by the name
field on the
Actor
type.
Now, to show the flexible nature of resolvers lets expand the the Actor
type:
type Actor {
name: String!
favoriteActor: Actor
}
Here we have the favoriteActor
of an Actor
being another Actor
. This demonstrates the how types can reference
each other and resolver functions have to be able to return the relevant data whether called from a Root Query or Root
Mutation type, or from another type, or the same type itself.
If Tom Hanks was Meryl Streep's favorite actor the favoriteActor
resolver could look like:
async function (parent, args, context, info) {
if (parent.name === "Meryl Streep") {
return "Tom Hanks";
}
return null;
}
Only when the parent Actor
has a name of "Meryl Streep"
do we return "Tom Hanks"
in other cases we will return
null
for all other Actor
s. This could be quite tedious to manage all the favorite actors from a single resolver so
it is usually best practice to have a data source or a well-referenced look-up that can be used. Typically, the parent
also has an ID
field that can be referenced for lookup also.
Default Field Resolver
It should be noted that GraphQL sets up every field with a default Field Resolver function. In fact it can be imported
from the graphql
package, too.
import { defaultFieldResolver } from 'graphql';
It will attempt to resolve a field based on the a look up of the field's name on the parent. This typically a desired behavior so often you don't have to write a resolver at all, graphql will do this "out of the box".
There are a couple of other things the default Field Resolver does, check out the source code, it's under 10 lines!
parent
parameter (first)
Sometimes called obj
, and be named whatever you call the variable, represents the previous returned value in the
"graph" of results. As mentioned, in root field resolvers this will typically be null since the query has just begun and
has no parent.
arg
parameter (second)
When a schema specifies arguments and those values are provided they are available on the field by its arg
parameter.
Instead of defaulting to "Meryl Streep" what if we had a list of actors required the name to be specified and returned
the actor that matched.
type Query {
# an additon of a required `name` argument to look up the actor
actor(name: String): Actor!
}
const actors = [ { name: 'Meryl Streep' }, { name: 'Tom Hanks' } ];
async function (parent, args, context, info) {
return actors.find(actor => actor.name === args.name);
}
And the value for name can be referenced as an argument:
query {
actor(name: "Tom Hanks") {
name
}
}
The source of this actors
data is a bit hand-wavy, there are ways of accessing these data sources (typically from the
context
parameter), but what is important is how arguments are passed to the field and its resolver via the args
parameter.
context
parameter (third)
This is the "global" bucket that is accessible in every resolver. It's usually an object. It's typically a good place to
put data sources that can combined with the parent
and args
can be used to lookup. graphql-mocks
has an opinion
approach with a managed context to provide conventions and assist in threading the common use cases.
For example, dependencies
that are passed to the graphql-mocks GraphQL Handler
can reliably be pulled from context using the
extractDependencies
function.
There are other helpful ways that context can be used with graphql-mocks
, too!
info
parameter (fourth)
This is mostly a holder of meta and GraphQL information. It's not usually needed but does have some handy insights into the query, AST, and access to the GraphQL schema.
Type Resolvers
While covered less the resolveType
Type Resolver function on the
GraphQLInterfaceType
and
GraphQLUnionType
play an important role in resolving GraphQL
queries. See the Apollo documentation for
additional examples.
Type Resolver functions have the following signature:
async function (value, context, info, abstractType) {
return 'ResolvedTypeName';
}
The objective for a Type Resolver is given a "value", the object in question, to determine which discrete type is
represented. The return value of a Type Resolver function is a string representing the concrete type name, ie: Dog
or
Cat
in the following example for the Pet
abstract type.
Here's an example:
schema {
query: Query
}
type Query {
pet: Pet!
}
union Pet = Cat | Dog
type Cat {
name: String!
}
type Dog {
name: String!
likesBones: Boolean!
}
And our Type Resolver:
async function (value, context, info, abstractType) {
if ('likesBones' in value) {
return 'Dog';
} else {
return 'Cat';
}
}
We are given the concrete object in question and are responsible for determining which type of Pet
it is: either a Pet
is a Dog
or a Cat
. In this contrived example the likesBones
property is checked to exist and if it does we assume
it's a Dog
. This is something to consider when working with Abstract Types (Unions and Interfaces) is how the
underlying concrete types will be identified. It is usually to leave a concrete type identifier __typename
on the
concrete type which would allow the default type resolver to automatically use the value.
value
parameter (first)
The ambiguous object in question that needs to be identified to a concrete type
context
parameter (second)
The same context
global object as represented in Field Resolvers.
info
parameter (third)
Represents the meta details and GraphQL information, is usually only needed in exceptional cases.
abstractType
parameter (fourth)
abstractType
is either a GraphQLInterfaceType
or GraphQLUnionType
and the instance provides the details of the
abstract type that is being resolved down to a concrete type.
Default Type Resolver
Similar to Field Resolvers, Type Resolvers also have a default resolver that exists on the resolveType
of the
Interface and Union types.
import { defaultTypeResolver } from 'graphql';
The default behavior the defaultTypeResolver
is to check the object for the __typename
field and return the value if
it exists, or less commonly to check isTypeOf
on each on each possible type with the object being coerced. Check out
the
source code
for more details.
Organizing Resolvers for Executing GraphQL queries
This page focuses on the Resolver functions themselves but hasn't shown how they are actually applied to the Fields and
Abstract Types that they are responsible for resolving. On their own they are "just functions" and need a way of being
assigned. This is the role of a Resolver Map which organized these Resolver Functions so that they can be applied to a
GraphQLSchema
and executed against queries and mutations, check out the
Resolver Map to understand the next step in applying Resolver functions.
Extending Resolvers with Resolver Wrappers
The next section will introduce Resolver Wrappers from this library and how they can be used to allow Resolvers functions to be extended.