Anatomy of an AppSync resolver

How request and response mapping work in AppSync

Author's image
Tamás Sallai
4 mins

Resolvers define how to provide the data for a GraphQL field. They provide the implementation behind the schema and as such they are important to get right.

In this article, we're going to take a detailed look into what happens in a resolver when it provides a value that AppSync can return to a query.

Data source

A resolver needs a data source. This defines what other service AppSync will use when it resolves the field. For example, the DynamoDB data source will send a request to a table, the HTTP data source will send a request to an arbitrary URL, and a Lambda data source calls a function.

The data source defines the request and the response formats. Fetching an item from a DynamoDB table requires different parameters than calling a Lambda function.

For example, to send a DynamoDB GetItem request using a DynamoDB data source requires this structure:

{
	"version" : "2017-02-28",
	"operation" : "GetItem",
	"key" : {
		"foo" : "... typed value",
		"bar" : "... typed value"
	},
	"consistentRead" : true
}

And its response contains the item with its fields converted to plain JSON types:

{
	"id": "hash key",
	"number": 15,
	"another_value": "test"
}

On the other hand, the HTTP data source requires a different request format:

{
	"version": "2018-05-29",
	"method": "PUT|POST|GET|DELETE|PATCH",
	"params": {
		"query": {},
		"headers": {},
		"body": "string"
	},
	"resourcePath": "string"
}

And its response contains the response body, the status code and a few other fields:

{
	"statusCode": 200,
	"body": "Test body"
}

The AppSync reference contains the expected request and response format for each data source.

The data source also configures how the target service can be used. For example, a DynamoDB data source needs to know which table to query, and what IAM role to use that has the necessary permissions. An HTTP data source needs the URL to call, and a Lambda data source needs to know the function to call.

Mapping templates

Mapping templates convert the AppSync query to the request format the data source requires as well as convert the data source response to the format AppSync expects for the field.

Both mapping templates get a context variable that contains information about the request and the response. AppSync uses VTL (Velocity Template Language) that gets this context and the template and transforms it to the result format.

AppSync provides some shorter alternatives for accessing the context. The $ctx is the same as $context, and $ctx.args is the same as $context.arguments.

Request template

Let's see an example!

There is a query that gets an ID and returns a User object:

type User {
	id: ID
	name: String
}

type Query {
	user(id: ID): User
}

schema {
	query: Query
}

The resolver for the Query.user field gets an id argument and uses a DynamoDB table as the data source. In the request mapping template, the ctx.args.id is the ID of the user the query asks for and because of the data source it needs to produce the GetItem structure described above.

Here's the request template for that resolver:

{
	"version" : "2018-05-29",
	"operation" : "GetItem",
	"key" : {
		"id": {"S": $util.toJson($ctx.args.id)}
	},
	"consistentRead" : true
}

Let's see what happens for a query!

query MyQuery {
	user(id: "user1@example.com") {
		id
		name
	}
}

When the request mapping template runs, $ctx.args.id will be "user1@example.com". So when AppSync transforms the template, the result will be:

{
	"version" : "2018-05-29",
	"operation" : "GetItem",
	"key" : {
		"id": {"S": "user1@example.com"}
	},
	"consistentRead" : true
}

You can inspect how the process works when AppSync logging is turned on. Notice the context and the transformedTemplate properties:

This matches what the DynamoDB data source expects, so it sends a request to the table (using an IAM role). When the item is fetched, it returns the object as JSON.

Response template

In the response mapping template, there will be two extra fields in the context object:

  • $ctx.error that indicates that DynamoDB returned an error if present
  • $ctx.result is the result object

AppSync won't return an error just because the underlying data source returned one, so the response template needs to check the $ctx.error and throw it if it is defined. Otherwise, it can return the $ctx.result:

#if ($ctx.error)
	$util.error($ctx.error.message, $ctx.error.type)
#end
$util.toJson($ctx.result)

How it works can be observed in the logs. Notice the context.result field and the transformedTemplate:

Conclusion

Resolvers are strongly tied to the data source configured for them. Data sources define the request and the response formats that AppSync needs to use.

The mapping templates define the transformation from the AppSync context to what the data source defines.

November 23, 2021
In this article