What are Resolvers in AppSync and how they work

How to provide the implementation for a GraphQL schema

Author's image
Tamás Sallai
2 mins

Resolvers in GraphQL

Resolvers provide the implementation for how AppSync processes queries and mutations. They connect the GraphQL schema, the abstract definition of the API, and the services that provide the data, such as a database or a Lambda function. In practice, most of AppSync development is spent writing resolvers.

Each resolver is for a field in the schema. And fields are defined for types. When a query requests a field, the resolver configured for that field runs and returns the appropriate data.

Let’s see an example!

There is a Query type and a test field that is a String. In GraphQL, it looks like this:

type Query {
	test: String
}

A query then can request the test field:

query MyQuery {
	test
}

In the response, the field will be a String, as defined in the schema:

{
	"data": {
		"test": "test response"
	}
}

Where does this value comes from? That’s what the resolver for the Query.test field defines. It can come from a database query, a Lambda function, an HTTP call, or some other place. The important thing is the response contains the string that AppSync will return to the query.

query MyQuery {test}"data": {"test": "test response"}Query.testQuery.testprocessrequestresponse"test response"AppSyncResolversDatabasesSimple query

Nested fields

What if a field returns a type instead of a scalar? For example, the item field returns an Item in this schema:

type Item {
	field1: String
}

type Query {
	item: Item
}

A query can then request the fields of the returned type:

query MyQuery {
	item {
		field1
	}
}

What happens here?

AppSync resolves fields recursively. It starts with the outermost field and moves down after that. Because of this, the first part is not different than before, as it resolves Query.item first.

The Query.item returns some response. For AppSync, it does not matter what exactly. But after that it moves down one level to Item.field1. The response needs to be a String and that will be the response for that field.

So, where is the result of the Query.item resolver used? The Item.field1 resolver gets that as the source object in the $context. In practice, most of the time the outer resolver returns an object and the inner resolver transforms part of it.

query MyQuery {item {field1}\t}item {field1}"data": {"item": {"field1": "test response"}\t}Query.itemQuery.itemItem.field1Item.field1processprocessrequestresponseobjectobjectrequestresponse"test response"AppSyncResolversDatabasesNested query

Why this structure is great

In a query, nesting can go to any depth, making it possible for the client to define exactly what it needs and AppSync does the rest. This does not requires any fewer database calls, but instead of having 1 backend call (between the client and AppSync) for each database query, it needs only 1 for the whole query. In practice, this drastically reduces the latency users experience.

Only 1 request is neededbrowserAPIdbGraphQL query for users and todoslist usersusersget todos for user 1user1 todosget todos for user 2user2 todosusers and todos

As a developer, you need to think about how to generate the response for individual fields and not for individual queries. You don’t need to know whether the client will need nested data or not. When a client sends a query, AppSync will run the necessary resolvers and provide the correct response with only the data the client needs.

09 November 2021
In this article