Resolvers

So far, all we've talked about were abstract concepts, such as the schema defining GraphQL constructs and client queries with fields that the response should contain. But how the backend goes from receiving the query and producing the response? Where does the data comes from? This is what the resolvers are for.

clientGraphQL backendqueryschemaRun resolversresponse
Resolvers provide the response to client queries

For example, the schema might define a user query that returns a User object:

# schema
type User {
  username: String!
  email: String
  bio: String
}

type Query {
  user(username: String!): User
}

Then the client sends a request with a query for a user:

# query
query MyQuery {
  user(username: "user1") {
    username
    bio
  }
}

What database query to send to retrieve this user object?

Of course, this is highly dependent on the architecture. Users might be stored in an SQL database, so fetching the one with the given username needs an SQL statement. Or they might be in DynamoDB, a common choice in AWS, and that needs a signed HTTPS request. Or a user directory, or an external system.

Resolvers define the mechanism that connect to the data sources behind the GraphQL API, such as databases, and provide the values for the queries. In practice, most of the time spent working on a GraphQL backend is spent writing resolvers.

How to write resolvers, such as the language they are written in, is highly dependent on the specific GraphQL server you are using. For example, GraphQL.js uses Javascript, and AWS AppSync uses Velocity templates (VTL) as the primary way.

In this chapter, we'll focus on the general concepts of how resolvers work that are the same for all implementations. Then we'll take a detailed look into how AWS AppSync works in the second part of the book.

A resolver provide a value for a field in the response. Fields, as we've covered in the Fields chapter, are both the properties of types (the username of a User type) and the queries or mutations (the user query).

Note

Resolvers provide values for fields.

Top-level fields

(Official docs)

Let's first consider the top-most part of a client query! This is the field of the Query type and that is the entry-point to the object graph.

Let's see it through an example!

The schema defines a query that returns a String:

# schema
type Query {
  test: String
}

Then a client sends a request to fetch this field:

# query
query MyQuery {
  test
}

When this client query reaches the GraphQL server, it looks at the top-level fields. Here, the test field of the Query type is the topmost one, so it will resolve that first.

query MyQuery {test}"data": {"test": "test response"}Query.testQuery.testprocessrequestresponse"test response"AppSyncResolversDatabases
Resolving the top-level field

That means a resolver is called, it gets some information about the query (such as the arguments), and provides a text response. What exactly that resolver does is up to the implementation, the only important point is that it must adhere to the type system: since the schema defines a String result, the resolver must return a String.

Nested fields

When a top-level field (a query or a mutation) returns a complex type, GraphQL needs to resolve the fields of the inner type too. For example, a query might return a User that has its own fields:

type User {
  username: String!
  email: String
  bio: String
}

type Query {
  user(username: String!): User
}

A query can define what inner fields it needs:

# query
query MyQuery {
  user(username: "user1") {
    username
    bio
    email
  }
}
clientGraphQLbackendQueryUserquery MyQuery {user(username: "user1"){usernamebioemail}}resolveQueryResolve userresolveUserResolve usernameResolve bioResolve email{"data": {"user": {"username": "user1","bio": "Lorem ipsum","email": "user@example.com"}}}
The GraphQL backend recursively resolves all fields in the response

Here, the first field to resolve is the Query.user. Its resolver runs, it goes to the database or wherever the user is stored then return this object:

{
  "username": "user1",
  "bio": "Hi there!"
}

Then GraphQL needs to resolve the User.username field, so it runs the resolver for that. This resolver gets the source object, which is what the parent resolver returned, in this case the JSON object with the username and the bio fields. Since the username is already in this object, it does not need to contact a database or a separate system but return that value. Then the same process happens with the User.bio field.

Note

Most servers implement a default resolver that simply returns the appropriate field from the source object. These are called trivial resolvers and they reduce the code you need to write. You can always overwrite these trivial resolvers with something custom.

The third field is the User.email. In our example, the database does not contain the email address, but there is a separate user directory that does, such as a Cognito User Pool. This means the resolver for this field needs to contact this external system and retrieve the email address for a user. This demonstrates the flexibility of this field-based architecture: even different fields for a type can come from different services.

When the email field is also resolved, the GraphQL backend now has everything it needs to send back the response.

clientGraphQLbackenddatabaseuserdirectoryquery MyQuery {user(username: "user1") {usernamebioemail}}resolve Query.userquery userusername = "user1"userresolve User.usernameuser.usernameresolve User.biouser.bioresolve User.emailget username = "user1"emailresponse
Each resolver can go to different databases
Note

Nested resolvers get the source object, which is the result of the parent resolver.

Resolver arguments

All resolver functions, no matter what language it is implemented, get at least these arguments:

  • the arguments in the query for the field
  • the source object for nested resolvers
  • metadata about the call

The query we used above:

# query
query MyQuery {
  user(username: "user1") {
    username
    bio
    email
  }
}

In the case of the Query.user resolver, these are:

  • the username: "user1" argument
  • no source object (top-level field)
  • metadata about the call, such as whether the caller is logged in, or the IP address

Since the resolver gets the username argument, it knows what object to query in the database. Also, as the metadata may contain login information, the resolver can decide whether or not to allow this operation. We'll cover this in detail in the Authentication chapter.

Then when the User.email resolver runs, it gets these parameters:

  • no arguments (no argument defined for the User.email field)
  • the User object
  • metadata about the call

Since the source object contains the username field, so the resolver knows what user to get from the directory.

Note

The parent resolver is free to return anything, as long as it is enough for the nested resolvers to provide values.

Lists

(Official docs)

Resolvers run for every field in the response. This means when a type is a list of a type, all its nested types will be resolved for each item.

For example, this query returns a list of users:

# schema
type User {
  username: String!
  email: String
  bio: String
}

type Query {
  allUsers: [User]
}

Then the client query can define what fields are needed for each object in the list:

# query
query MyQuery {
  allUsers {
    username
    bio
  }
}

The GraphQL backend resolves the top-level field (Query.allUsers) that returns a list, then it resolves the username and the bio for every item in that list. In that case, the nested resolvers get one item of the list as the source object.

clientGraphQLbackendQueryUserquery MyQuery {allUsers {usernamebio}}resolve QueryResolve allUsers[{"username": "user1", "bio": "..."},{"username": "user2", "bio": "..."}]resolve User 1Resolve usernameResolve bioresolve User 2Resolve usernameResolve bioresponse
Nested resolvers run for every element in a collection
Master AppSync and GraphQL
Support this book and get all future updates and extra chapters in ebook format.