DEV Community

Cover image for Use lambda resolvers in your GraphQL API with AWS Amplify
Nxtra for AWS Community Builders

Posted on • Originally published at theclouddeveloper.io on

Use lambda resolvers in your GraphQL API with AWS Amplify

Written by Nick Van Hoof. Big thanks to Matthew Marks for the review.

TL;DR - Find out how to update your API resources and corresponding DynamoDB tables with a lambda resolver!

Architecture

The use case

You have a posts with comments.
When you remove a post, you want to remove the comments too.

How do you do that with one graphql mutation?
Use a LAMBDA RESOLVER!


To put it more difficult 😄 :

I want to show you how you can make an update to multiple of your Amplify API @model-annotated types (and corresponding DynamoDB tables) in one graphql request.

The API

Aight, let's see our API definition. (I took this straight from the Amplify docs and made my additions with the custom query)

Hooray!

  • A custom mutation to delete a post and corresponding comments.
  • The response of this mutation containing the id of the deleted post.
  • The @function directive which create a pipeline resolver to resolve our custom mutation.

If you want to follow along, run amplify add api and use the schema above.

But hold your horses, do not deploy. This won't work yet. We first have to create the function.

The Lambda Function

Since we used the @function directive Amplify will make sure that AppSync resolves the deletePostAndComments mutation using the Lambda Function.

Let's create that function, shall we?!

In the function we follow these steps:

  • Import the name of our model tables. Via environment variables. We'll see later how to add these.
  • Specify a resolver for the mutation type and deletePostAndComments field.
  • In our handler, select the right resolver.
  • Execute the resolver logic and return the id of the deleted post

Here it is. More explanation below! 😍

A brief word on the resolver logic that is executed.

  • Find all comments linked to this post by querying the GSI byPost that is created in the API specification in schema.graphql.
  • Execute the requests to remove all corresponding comments and the post. Notice that we execute these request in parallel by resolving the promises together.
  • The comments are deleted via the batchWrite API of DynamoDB. A post might have tens or hundreds of comments. You don't want to execute hundreds of separate requests (because of latency). Kudos to Nader Dabit for providing the Document Client Cheat sheet (find the link in the resources)

Now add the function using the Amplify cli.

amplify add function
? Select which capability you want to add: Lambda function (server
less function)
? Provide an AWS Lambda function name: editPostAndComments
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? Yes
Enter fullscreen mode Exit fullscreen mode

And add the code above in index.js.

Add this point you can run amplify push for the first time.

However, did you notice that we chose the Hello World template. This won't configure the necessary access rights to the DynamoDB resources. Neither of the templates will, we'll do this ourselves in the section below. Also, we need environment variables for our Lambda function!

Read on to find out how to setup custom permissions and environment variables.

Configuring our Lambda Function

Now we dive deep into working with Amplify! 💁

Adding the function to our project created a bunch of files to configure the function.

Nothing to be scared of, yet it can frighten new users:

  • editPostAndComments-cloudformation-template.json : contains the cloudformation that defines all the resources related to this function eg. role, permission, triggers, the function itself..
  • parameters.json: a file that we will create below. Contains the parameters that will be past into this template coming from other cloudformation stacks.

You guessed it, we have to add our environment variables and permissions to this cloudformation template.

In the parameters inside editPostAndComments-cloudformation-template.json add:

"apiGraphQLAPIIdOutput": {
  "Type": "String"
}
Enter fullscreen mode Exit fullscreen mode

This will be the parameter that receives the Id of your AppSync GraphQL API.

Under environment variables inside editPostAndComments-cloudformation-template.json add:

"POSTTABLE": {
  "Fn::ImportValue": {
    "Fn::Sub": "${apiGraphQLAPIIdOutput}:GetAtt:PostTable:Name"
  }
},
"COMMENTTABLE": {
  "Fn::ImportValue": {
    "Fn::Sub": "${apiGraphQLAPIIdOutput}:GetAtt:CommentTable:Name"
  }
},
"AWS_NODEJS_CONNECTION_REUSE_ENABLED": 1
Enter fullscreen mode Exit fullscreen mode

Here we import the table names which are exported by our API stacks. Also notice the CONNECTION_REUSE environment variable; This will make sure connections with DynamoDB are reused in our function instead of opening and closing all the time. Hence, performance gain! 😎

Now let's add the policies with the permissions for accessing our dynamodb API resources: Under resources inside editPostAndComments-cloudformation-template.json add:

This grants access to execute the queries that are specified in the lambda's index.js. Mind that we need access to the tables, but also to the index byPost on the comments table.

People often think that custom configuration in editPostAndComments-cloudformation-template.json will be overwritten with every push.

This is NOT TRUE.

  • Custom configuration is not overwritten on running amplify push.
  • Name your custom resources, here AccessDynamoDBApiResourcesPolicy, with a name that is not used as a standard by the framework (eg. lambdaexecutionpolicy; don't use lambdaexecutionpolicy for your custom policy). Now your policy will survive even when running amplify fuction update for our function.

As long as you follow these guidelines, you are pretty safe.

Next add a file parameters.json next to editPostAndComments-cloudformation-template.json

{
  "apiGraphQLAPIIdOutput": {
    "Fn::GetAtt": ["api<nameOfYourAppSyncAPI>", "Outputs.GraphQLAPIIdOutput"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we are telling Amplify to get the outputted GraphQLAPIIdOutput from the api<nameOfYourAppSyncAPI> stack. We are basically connecting our stacks here by referencing a variable from another stack which will be passed into our template.

And we have lift off! Run amplify push.

Testing our setup

Log into the AWS console, go to your lambda function and verify that it has the correct permissions and environment variables.

Go to the AppSync console! Create a post:

mutation CreatePost {
  createPost(input: {blogID: "dfeebd0d-3d18-4e3e-9d10-88fcf0fb8b4d", title: "Hello Cloud"}) {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

Specify the returned id as query variable:

{
  "postId": "50571e27-e0f3-48ce-9432-c7eb0307010d"
}
Enter fullscreen mode Exit fullscreen mode

Create some comments for the post:

mutation CreateCommentsForPost($postId: ID!) {
  create1: createComment(input: {postID: $postId, content: "This is a short comment"}) {
    id
  }
  create2: createComment(input: {postID: $postId, content: "This is a second comment"}) {
    id
  }
  create3: createComment(input: {postID: $postId, content: "This is the last comment"}) {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

And now for the big moment. Delete the post and its comments at once!

mutation DeletePostAndComments($postId: ID!) {
  deletePostAndComments(postId: $postId) {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

😱 Nice, that should look like:

If you made that work, you are a ⭐!
If you did not make it work, but you made it till here in the blogpost you are also a ⭐.

If you need guidance, reach out to me: Nick on Twitter 🐥

Testing performance

Let's say you have a post which has 500 comments! When deleting the post and the comments via a synchronous api call, how much time would that take.

Well, let's test it out!

I created a post and added a 500 comments. If you look into the logic of the Lambda function you'll find that the comments are deleted using the batchWrite API of DynamoDB. Even the batchWrite API request can only contain 25 requests at once, meaning we need 20 batchWrite calls to delete 500 comments.

That is still quite a lot! 😯 Let's see how it performs via AWS Xray:

It took about 600 ms to resolve the deletePostAndComments mutation for 1 post with 500 comments using a Lambda Function with 512 Mb of memory configured.

If executing your Graphql mutation always has to update (or delete) this many (or more) items, there is a point at which it becomes a better solution to execute that work asynchronously.

You could delete a post using a normal VTL resolver generated by Amplify. Then you can catch the delete event of the DynamoDB change stream to delete the comments with a Lambda function. Something for the next blog. 😏

Resources

Credits

Top comments (4)

Collapse
 
devtghosh profile image
Devjyoti Ghosh • Edited

For people that might run into the issue of it taking too much time to execute the function, you may wanna trying increasing the ram of the lambda function from the AWS console. The CPU is tied directly to the ram so the higher the ram better the CPU.

Lambda billing is accurate to 100-ms increments. For that reason, when allocating memory to a function, you need to consider that putting the smallest RAM may reduce the memory cost but increase latency. It may outweigh the cost savings due to the longer execution time of the function.

Thanks for the great article @nxtra

Collapse
 
jonathanbravecredit profile image
jonathanbravecredit

Because you are using the DynamoDB client directly to delete the records, does Appsync subscriptions notify the client that the records have been deleted so it is in sync with the db?

Collapse
 
rpostulart profile image
rpostulart • Edited

Hi,

This is a nice written blog which is very helpful.
Any ideas when working with multi env? Because you don't want to do these manual tasks each time you deploy to a new environment.

Collapse
 
enthusiast_tech profile image
Rajat

Nice post really! just one question - can you tell if it is efficient and advisable to use @model, @connection, @searchable on a production application or they're only more suited for POCs?