AWS AppSync
AWS AppSync Developer Guide

Tutorial: AWS Lambda Resolvers

AWS AppSync enables you to use AWS Lambda to resolve any GraphQL field. For example, a GraphQL query might send a call to an Amazon RDS instance, and a GraphQL mutation might write to a Amazon Kinesis stream. In this section, we'll show you how to write a Lambda function that performs business logic based on the invocation of a GraphQL field operation.

Create a Lambda Function

The following example shows a Lambda function written in Node.js that performs different operations on blog posts as part of a blog post application example.

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got an Invoke Request."); switch(event.field) { case "getPost": var id = event.arguments.id; callback(null, posts[id]); break; case "allPosts": var values = []; for(var d in posts){ values.push(posts[d]); } callback(null, values); break; case "addPost": // return the arguments back callback(null, event.arguments); break; case "addPostErrorWithData": var id = event.arguments.id; var result = posts[id]; // attached additional error information to the post result.errorMessage = 'Error with the mutation, data has changed'; result.errorType = 'MUTATION_ERROR'; callback(null, result); break; case "relatedPosts": var id = event.source.id; callback(null, relatedPosts[id]); break; default: callback("Unknown field, unable to resolve" + event.field, null); break; } };

This Lambda function handles retrieving a post by ID, adding a post, retrieving a list of posts, and fetching related posts for a given post.

Note: The switch statement on event.field enables the Lambda function to determine which field is being currently resolved.

Now let's create this Lambda function using the AWS Management Console or with AWS CloudFormation by choosing the following:

aws cloudformation create-stack --stack-name AppSyncLambdaExample \ --template-url https://s3-us-west-2.amazonaws.com/awsappsync/resources/lambda/LambdaCFTemplate.yaml \ --capabilities CAPABILITY_NAMED_IAM

You can launch this AWS CloudFormation stack in the US West 2 (Oregon) Region in your AWS account:

Configure Data Source for AWS Lambda

After the AWS Lambda function has been created, navigate to your AWS AppSync GraphQL API in the console and choose the Data Sources tab.

Choose New, enter a friendly name for the data source (for example, "Lambda"), and then choose AWS Lambda in Data source type. Choose the appropriate AWS Region, and then you should see your Lambda functions listed.

After selecting your Lambda function, you can either create a new role (for which AWS AppSync assigns the appropriate permissions) or choose an existing role that has the following inline policy:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": "arn:aws:lambda:REGION:ACCOUNTNUMBER:function/LAMBDA_FUNCTION" } ] }

You also need to set up a trust relationship with AWS AppSync for that role as follows:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }

Creating a GraphQL Schema

Now that the data source is connected to your Lambda function, let's create a GraphQL schema.

From the schema editor in the AWS AppSync console, make sure you schema matches the following schema:

schema { query: Query mutation: Mutation } type Query { getPost(id:ID!): Post allPosts: [Post] } type Mutation { addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int relatedPosts: [Post] }

Configuring Resolvers

Now that we've registered an AWS Lambda data source and a valid GraphQL schema, we can connect our GraphQL fields to our Lambda data source using resolvers.

To create a resolver, we'll need mapping templates. To learn more about mapping templates, read AWS AppSync mapping templates overview Resolver Mapping Template Overview.

For more information about Lambda mapping templates, see Resolver Mapping Template Reference for Lambda.

In this step, you attach a resolver to the Lambda function for the following fields: getPost(id:ID!): Post, allPosts: [Post], addPost(id: ID!, author: String!, title: String, content: String, url: String): Post!, and Post.relatedPosts: [Post].

From the schema editor in the AWS AppSync console, on the right side choose Attach Resolver for getPost(id:ID!): Post.

Choose your AWS Lambda data source. In the request mapping template section, choose Invoke And Forward Arguments.

Modify the payload object to add the field name. Your template should look like the following:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "getPost", "arguments": $utils.toJson($context.arguments) } }

In the response mapping template section, choose Return Lambda Result.

In this case, use the base template as-is. It should look like the following:

$utils.toJson($context.result)

Choose Save. You have successfully attached your first resolver. Repeat this operation for the remaining fields as follows:

For addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! request mapping template:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "addPost", "arguments": $utils.toJson($context.arguments) } }

For addPost(id: ID!, author: String!, title: String, content: String, url: String): Post! response mapping template:

$utils.toJson($context.result)

For allPosts: [Post] request mapping template:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "allPosts" } }

For allPosts: [Post] response mapping template:

$utils.toJson($context.result)

For Post.relatedPosts: [Post] request mapping template:

{ "version": "2017-02-28", "operation": "Invoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

For Post.relatedPosts: [Post] response mapping template:

$utils.toJson($context.result)

Testing Your GraphQL API

Now that your Lambda function is connected to GraphQL resolvers, you can run some mutations and queries using the console or a client application.

On the left side of the AWS AppSync console, choose the Queries tab, and then paste in the following code:

addPost Mutation

mutation addPost { addPost( id: 6 author: "Author6" title: "Sixth book" url: "https://www.amazon.com/" content: "This is the book is a tutorial for using GraphQL with AWS AppSync." ) { id author title content url ups downs } }

getPost Query

query { getPost(id: "2") { id author title content url ups downs } }

allPosts Query

query { allPosts { id author title content url ups downs relatedPosts { id title } } }

Returning Errors

Any given field resolution can result in an error. AWS AppSync enables you raise errors from the following sources:

  • Request or response mapping template

  • Lambda function

From the Mapping Template

You can use the $utils.error helper method from the VTL template to raise intentional errors. It takes as argument an errorMessage, an errorType, and an optional data value. The data is useful for returning extra data back to the client when an error occurs. The data object is added to the errors in the GraphQL final response.

The following example shows how to use it in the Post.relatedPosts: [Post] response mapping template:

$utils.error("Failed to fetch relatedPosts", "LambdaFailure", $context.result)

This yields a GraphQL response similar to the following:

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "LambdaFailure", "locations": [ { "line": 5, "column": 5 } ], "message": "Failed to fetch relatedPosts", "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ] } ] }

Where allPosts[0].relatedPosts is null because of the error and the errorMessage, errorType, and data are present in the data.errors[0] object.

From the Lambda Function

AWS AppSync also understands errors thrown from the Lambda function. The Lambda programming model lets you raise handled errors. If an error is thrown from the Lambda function, AWS AppSync fails to resolve the current field. Only the error message returned from Lambda will be set in the response. Currently, you can't pass any extraneous data back to the client by raising an error from the Lambda function.

Note: If your Lambda function raises an unhandled error, AWS AppSync uses the error message set by AWS Lambda.

The following Lambda function raises an error:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); callback("I fail. Always."); };

This returns a GraphQL response similar to the following:

{ "data": { "allPosts": [ { "id": "2", "title": "Second book", "relatedPosts": null }, ... ] }, "errors": [ { "path": [ "allPosts", 0, "relatedPosts" ], "errorType": "Lambda:Handled", "locations": [ { "line": 5, "column": 5 } ], "message": "I fail. Always." } ] }

Advanced Use Case: Batching

The Lambda function in this example has a relatedPosts field that returns a list of related posts for a given post. In the example queries, the allPosts field invocation from the Lambda function returns five posts. Because we have specified that we also want to resolve relatedPosts for each returned post, the relatedPosts field operation will, in turn, be invoked five times.

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yields 5 posts id title } } }

While this doesn't sound substantial in this specific example, the application can be undermined quickly by this compounded over-fetching.

If you were to fetch relatedPosts again on the returned related Posts in the same query, the number of invocations would increase dramatically.

query { allPosts { // 1 Lambda invocation - yields 5 Posts id author title content url ups downs relatedPosts { // 5 Lambda invocations - each yield 5 posts = 5 x 5 Posts id title relatedPosts { // 5 x 5 Lambda invocations - each yield 5 posts = 25 x 5 Posts id title author } } } }

In this relatively simple query, AWS AppSync would invoke the Lambda function 1 + 5 + 25 = 31 times.

This is a fairly common challenge and is often called the N+1 problem (in this case, N = 5), and it can incur increased latency and cost to the application.

One approach to solving this issue is to batch similar field resolver requests together. In this example, instead of having the Lambda function resolve a list of related posts for a single given post, instead it could resolve a list of related posts for a given batch of posts.

To demonstrate this, let's switch the Post.relatedPosts: [Post] resolver to a batch-enabled resolver.

On the right side of the AWS AppSync console, choose the existing Post.relatedPosts: [Post] resolver. Change the request mapping template to the following:

{ "version": "2017-02-28", "operation": "BatchInvoke", "payload": { "field": "relatedPosts", "source": $utils.toJson($context.source) } }

Only the operation field has changed from Invoke to BatchInvoke. The payload field now becomes an array of whatever has been specified in the template. In this example, the Lambda function receives the following as input:

[ { "field": "relatedPosts", "source": { "id": 1 } }, { "field": "relatedPosts", "source": { "id": 2 } }, ... ]

When BatchInvoke is specified in the request mapping template, the Lambda function receives a list of requests and returns a list of results.

Specifically, the list of results must match the size and order of the request payload entries so that AWS AppSync can match the results accordingly.

In this batching example, the Lambda function returns a batch of results as follows:

[ [{"id":"2","title":"Second book"}, {"id":"3","title":"Third book"}], // relatedPosts for id=1 [{"id":"3","title":"Third book"}] // relatedPosts for id=2 ]

The following AWS Lambda function in Node.js demonstrates this batching functionality for the Post.relatedPosts field as follows:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; // the response MUST contain the same number // of entries as the payload array for (var i=0; i< event.length; i++) { console.log("post {}", JSON.stringify(event[i].source)); results.push(relatedPosts[event[i].source.id]); } console.log("results {}", JSON.stringify(results)); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

Returning Individual Errors

The previous examples show that it's possible to return a single error from the Lambda function or raise an error from the mapping templates. For batched invocations, raising an error from the Lambda function flags an entire batch as failed. This might be acceptable for specific scenarios where an irrecoverable error occurs, such as, a failed connection to a data store. However, in cases where some items in the batch succeed and some others fail, it's possible to return both errors and valid data. Because AWS AppSync requires that the batch response list elements matching the original size of the batch, you need to define a data structure that can differentiate valid data from an error.

For example, if the Lambda function is expected to return a batch of related posts, you could choose to return a list of Response objects where each object has optional data, errorMessage, and errorType fields. If the errorMessage field is present, it means that an error occurred.

The following code shows how you could update Lambda function:

exports.handler = (event, context, callback) => { console.log("Received event {}", JSON.stringify(event, 3)); var posts = { "1": {"id": "1", "title": "First book", "author": "Author1", "url": "https://amazon.com/", "content": "SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1 SAMPLE TEXT AUTHOR 1", "ups": "100", "downs": "10"}, "2": {"id": "2", "title": "Second book", "author": "Author2", "url": "https://amazon.com", "content": "SAMPLE TEXT AUTHOR 2 SAMPLE TEXT AUTHOR 2 SAMPLE TEXT", "ups": "100", "downs": "10"}, "3": {"id": "3", "title": "Third book", "author": "Author3", "url": null, "content": null, "ups": null, "downs": null }, "4": {"id": "4", "title": "Fourth book", "author": "Author4", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4 SAMPLE TEXT AUTHOR 4", "ups": "1000", "downs": "0"}, "5": {"id": "5", "title": "Fifth book", "author": "Author5", "url": "https://www.amazon.com/", "content": "SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT AUTHOR 5 SAMPLE TEXT", "ups": "50", "downs": "0"} }; var relatedPosts = { "1": [posts['4']], "2": [posts['3'], posts['5']], "3": [posts['2'], posts['1']], "4": [posts['2'], posts['1']], "5": [] }; console.log("Got a BatchInvoke Request. The payload has %d items to resolve.", event.length); // event is now an array var field = event[0].field; switch(field) { case "relatedPosts": var results = []; results.push({ 'data': relatedPosts['1'] }); results.push({ 'data': relatedPosts['2'] }); results.push({ 'data': null, 'errorMessage': 'Error Happened', 'errorType': 'ERROR' }); results.push(null); results.push({ 'data': relatedPosts['3'], 'errorMessage': 'Error Happened with last result', 'errorType': 'ERROR' }); callback(null, results); break; default: callback("Unknown field, unable to resolve" + field, null); break; } };

For this example, the following response mapping template parses each item of the Lambda function and raises any errors that occur:

#if( $context.result && $context.result.errorMessage ) $utils.error($context.result.errorMessage, $context.result.errorType, $context.result.data) #else $utils.toJson($context.result.data) #end

This example returns a GraphQL response similar to the following:

{ "data": { "allPosts": [ { "id": "1", "relatedPostsPartialErrors": [ { "id": "4", "title": "Fourth book" } ] }, { "id": "2", "relatedPostsPartialErrors": [ { "id": "3", "title": "Third book" }, { "id": "5", "title": "Fifth book" } ] }, { "id": "3", "relatedPostsPartialErrors": null }, { "id": "4", "relatedPostsPartialErrors": null }, { "id": "5", "relatedPostsPartialErrors": null } ] }, "errors": [ { "path": [ "allPosts", 2, "relatedPostsPartialErrors" ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened" }, { "path": [ "allPosts", 4, "relatedPostsPartialErrors" ], "data": [ { "id": "2", "title": "Second book" }, { "id": "1", "title": "First book" } ], "errorType": "ERROR", "locations": [ { "line": 4, "column": 9 } ], "message": "Error Happened with last result" } ] }