loading...

Lambda Function GraphQL Resolvers

dabit3 profile image Nader Dabit Updated on ・7 min read

The Amplify CLI recently added support for deploying Lambda GraphQL resolvers directly from your Amplify environment for your AppSync APIs. Lambda function resolvers allow you to write your AppSync resolver logic in JavaScript.

Using the @function directive you can specify operations to interact with a Lambda function in your GraphQL schema:

type Mutation {
  addEntry(id: Int, email: String): String @function(name: "addEntry-${env}")
}

In this tutorial, I'll teach you how to create an application that uses two types of Lambda resolvers:

  1. A Lambda resolver that talks to another API and returns a GraphQL response via a Query

  2. A Lambda resolver that sends Queries and Mutations to interact with a real NoSQL database to perform Create and Read operations against it.

By the end of this tutorial, you should understand how to deploy an AppSync GraphQL API that interacts with Lambda GraphQL resolvers using the Amplify Framework.

To view the final source code for this project, click here.

Getting Started

To start things off, you'll need to create a new React application and initialize a new Amplify project within it:

npx create-react-app gql-lambda

cd gql-lambda

amplify init

# Follow the steps to give the project a name, environment name, and set the default text editor.
# Accept defaults for everything else and choose your AWS Profile.

If you don't yet have the Amplify CLI installed and configured, follow the directions here.

Next, install the AWS Amplify library:

npm install aws-amplify

Creating the API

The first GraphQL API we'll create is one that will query data from another REST API and return a GraphQL response. The API that you'll be interacting with is the Coinlore API.

Let's first create the function:

amplify add function

? Provide a friendly name for your resource to be used as a label for this category in the project: currencyfunction
? Provide the AWS Lambda function name: currencyfunction
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello world
? Do you want to access other resources created in this project from your Lambda function? N
? Do you want to invoke this function on a recurring schedule? N
? Do you want to edit the local lambda function now? Y

Update the function with the following code:

// amplify/backend/function/currencyfunction/src/index.js
const axios = require('axios')

exports.handler = function (event, _, callback) {
  let apiUrl = `https://api.coinlore.com/api/tickers/?start=1&limit=10`

  if (event.arguments) { 
    const { start = 0, limit = 10 } = event.arguments
    apiUrl = `https://api.coinlore.com/api/tickers/?start=${start}&limit=${limit}`
  }

  axios.get(apiUrl)
    .then(response => callback(null, response.data.data))
    .catch(err => callback(err))
}

In the above function we've used the axios library to call another API. In order to use axios, we need to install it in the function folder. We'll also install uuid for later use:

cd amplify/backend/function/currencyfunction/src

npm install axios uuid

cd ../../../../../

Now that the function has been created, we'll need to create the GraphQL API. To do so, run the Amplify add command:

amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: currencyapi
? Choose an authorization type for the API: API key
? Enter a description for the API key: public
? After how many days from now the API key should expire (1-365): 365 (or your preferred expiration)
? Do you want to configure advanced settings for the GraphQL API: N
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

Next, in amplify/backend/api/currencyapi/schema.graphql, update the schema with the following:

type Coin {
  id: String!
  name: String!
  symbol: String!
  price_usd: String!
}

type Query {
  getCoins(limit: Int start: Int): [Coin] @function(name: "currencyfunction-${env}")
}

Now the API and Lambda function have both been created. To deploy them and make them live, you can run the push command:

amplify push

Current Environment: dev

| Category | Resource name    | Operation | Provider plugin   |
| -------- | -------------    | --------- | ----------------- |
| Api      | currencyapi      | Create    | awscloudformation |
| Function | currencyfunction | Create    | awscloudformation |
? Are you sure you want to continue? (Y/n) Y

Now, the resources have been deployed and you can try out the query! You can test the query out in the AWS AppSync console. To open the API dashboard, run the following command in your terminal:

amplify console api

? Please select from one of the below mentioned services: GraphQL

In the query editor, run the following queries:

# basic request
query listCoins {
  getCoins {
    price_usd
    name
    id
    symbol
  }
}

# request with arguments
query listCoinsWithArgs {
  getCoins(limit:3 start: 10) {
    price_usd
    name
    id
    symbol
  }
}

This query should return an array of cryptocurrency information.

Updating the API to perform CRUD operations against a NoSQL database

Now that the basic API is up and running, let's create a database and update the API to perform create and read operations against it.

To get started, we'll create the database:

amplify add storage

? Please select from one of the below mentioned services: NoSQL Database
? Please provide a friendly name for your resource that will be used to label this category in the project: currencytable
? Please provide table name: currencytable
? What would you like to name this column: id
? Please choose the data type: string
? Would you like to add another column? Y
? What would you like to name this column: name
? Please choose the data type: string
? Would you like to add another column? Y
? What would you like to name this column: symbol
? Please choose the data type: string
? Would you like to add another column? Y
? What would you like to name this column: price_usd
? Please choose the data type: string
? Would you like to add another column? N
? Please choose partition key for the table: id
? Do you want to add a sort key to your table? N
? Do you want to add global secondary indexes to your table? N
? Do you want to add a Lambda Trigger for your Table? N

Next, let's update the function to use the new database.

amplify update function

? Please select the Lambda Function you would want to update: currencyfunction
? Do you want to update permissions granted to this Lambda function to perform on other resources in your project? Y
? Select the category: storage
? Select the operations you want to permit for currencytable:
  ◉ create
  ◉ read
  ◉ update
 ❯◉ delete
? Do you want to invoke this function on a recurring schedule? N
? Do you want to edit the local lambda function now? Y

Next, we'll update the lambda function. Right now the function code lives on only one file, index.js located at amplify/backend/function/currencyfunction/src/index.js. In the src folder, create two new files: createCoin.js and getCoins.js. In the next steps, we'll update index.js and also populate the other two new files with code.

index.js

const getCoins = require('./getCoins')
const createCoin = require('./createCoin')

exports.handler = function (event, _, callback) {
  if (event.typeName === 'Mutation') {
    createCoin(event, callback)
  }
  if (event.typeName === 'Query') {
    getCoins(callback)
  }
}

In the event argument to the function, there is a typeName field that will tell us if the operation is a Mutation or Query. There is also a fieldName argument that will tell you the actual field being executed if you have multiple Queries or Mutations.

We will use the typeName field to call either createCoin or getCoins based on the type of operation.

getCoins.js

const AWS = require('aws-sdk')
const region = process.env.REGION
const storageCurrencytableName = process.env.STORAGE_CURRENCYTABLE_NAME
const docClient = new AWS.DynamoDB.DocumentClient({region})

const params = {
  TableName: storageCurrencytableName
}

function getCoins(callback) {
  docClient.scan(params, function(err, data) {
    if (err) {
      callback(err)
    } else {
      callback(null, data.Items)
    }
  });
}

module.exports = getCoins

In getCoins we call a DynamoDB scan operation to read the database and return all of the values in an array. We also use the DynamoDB.DocumentClient sdk to simplify working with items in Amazon DynamoDB with JavaScript.

createCoin.js

const AWS = require('aws-sdk')
const { v4: uuid } = require('uuid')
const region = process.env.REGION
const ddb_table_name = process.env.STORAGE_CURRENCYTABLE_NAME
const docClient = new AWS.DynamoDB.DocumentClient({region})

function write(params, event, callback){
  docClient.put(params, function(err, data) {
    if (err) {
      callback(err)
    } else {
      callback(null, event.arguments)
    }
  })
}

function createCoin(event, callback) {
  const args = { ...event.arguments, id: uuid() }
  var params = {
    TableName: ddb_table_name,
    Item: args
  };

  if (Object.keys(event.arguments).length > 0) {
    write(params, event, callback)
  } 
}

module.exports = createCoin

In createCoin we do a putItem operation against the DynamoDB table passing in the arguments. We also auto-generate and ID on the server to populate a unique ID for the item using the uuid library.

Finally, we'll update the GraphQL Schema at amplify/backend/api/currencyapi/schema.graphql to add the mutation definition:

# amplify/backend/api/currencyapi/schema.graphql

type Coin {
  id: String!
  name: String!
  symbol: String!
  price_usd: String!
}

type Query {
  getCoins(limit: Int start: Int): [Coin] @function(name: "currencyfunction-${env}")
}

# new mutation definition
type Mutation {
  createCoin(name: String! symbol: String! price_usd: String!): Coin @function(name: "currencyfunction-${env}")
}

Now, deploy the changes:

amplify push

Testing it out

Now, the resources have been deployed and you can try out the query! You can test the query out in the AWS AppSync console. To open your project, run the following command in your terminal:

amplify console api

? Please select from one of the below mentioned services: GraphQL

Test out the following queries:

query listCoins {
  getCoins {
    price_usd
    name
    id
    symbol
  }
}

mutation createCoin {
  createCoin(
    name: "Monero"
    price_usd: "86.85"
    symbol: "XMR"
  ) {
    name price_usd symbol
  }
}

Testing it out on the client

If you'd like to test it out in the React application, you can use the API category from Amplify:

import { API, graphqlOperation } from 'aws-amplify'
import { getCoins } from './graphql/queries'
import { createCoin } from './graphql/mutations'

// mutation
const coin = { name: "Bitcoin", symbol: "BTC", price: "10000" }
API.graphql(graphqlOperation(createCoin, coin))
  .then(data => console.log({ data }))
  .catch(err => console.log('error: ', err))

// query
API.graphql(graphqlOperation(getCoins))
  .then(data => console.log({ data }))
  .catch(err => console.log('error: ', err))

To view the final source code for this project, click here.

Discussion

pic
Editor guide
Collapse
jimcalliu profile image
𝓙𝓲𝓶 𝓛𝓲𝓾

Followup: I like to change the name of project/function when follow tutorial, this time, however, I discover if I don't use function in lambda name, the cloudformation could not actually create the stack.

I went ahead and redo the first part of tutorial, but run into an issue when running amplify push after I run amplify update api (instead of amplify add api I had to use update because the api has already been created.)

Current Environment: dev

| Category | Resource name    | Operation | Provider plugin   |
| -------- | ---------------- | --------- | ----------------- |
| Function | currencyfunction | Create    | awscloudformation |
| Function | currency         | No Change | awscloudformation |
| Api      | currencyapi      | No Change | awscloudformation |
? Are you sure you want to continue? Yes
✖ An error occurred when pushing the resources to the cloud

Inaccessible host: `gql-lambda-dev-20190623145233-deployment.s3.us-west-2.amazonaws.com'. This service may not be available in the `us-west-2' region.

Any pointer would be appreciated!

Collapse
jimcalliu profile image
𝓙𝓲𝓶 𝓛𝓲𝓾

Hey Nader,

Excellent tutorial. I was following along and use amplify push to deploy the Lambda. After I type Y to "Are you sure you want to continue", I saw the following question and stuck on the last one.

? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions (src/gra
phql/**/*.js)

Any recommendation? Where can I find the documentation for the Amplify API and its behavior?

Really excited about what AWS Mobile team has build. Please keep up the great work.

Collapse
yiksanchan profile image
yiksanchan

Is it possible to access s3/dynamodb configured in an existing amplify project via @function directive? github.com/aws-amplify/amplify-cli...

Collapse
diego profile image
Diego

It doesn't seem to be possible at the moment. I wonder if we can expect this functionality in future versions.

Collapse
cedricindra profile image
Cedric Indra

Doing this tutorial and things have changed in the amplify CLI so I'm trying to work around that because I like the tone and pace of these tuts.
When updating function, cannot figure out how to "Select the operations you want to permit for currencytable:" Seems that is deprecated.
If anyone can point me to a fix.

Collapse
boricic profile image
Milan Boricic

This is awesome. There is still one part that I haven't been able to figure out.

How does STORAGE_CURRENCYTABLE_NAME environment variable get created? I've looked at the Github repo, but couldn't find a place where it is defined. I can just see that it is accessed within Lambda code.

I assume it was created through Amplify Console, right?

Collapse
karldanninger profile image
Karl Danninger 🚀

I think I found it here: console.aws.amazon.com/lambda/home there's a section halfway down for environment variables that you can add. Nader must've made a custom one in the console.

Collapse
ganapathiraju profile image
ganapathiraju

Hello Nader Dabit,
Good article from the rest of the articles w.r.t resolving graphql and lamda function. Appreciate it.
After following your steps, I got blocked at the first query step from console as the query is throwing exception Lambda unhandler. Therefore I did not go ahead to complete the application. Where could I go wrong?
Running the query listCoins from console->queries is resulting as below.
if the url 'api.coinlore.com/api/tickers/?star...' is executed from "Postman" client then the result is proper.
{
"data": {
"getCoins": null
},
"errors": [
{
"path": [
"getCoins"
],
"data": null,
"errorType": "Lambda:Unhandled",
"errorInfo": null,
"locations": [
{
"line": 31,
"column": 3,
"sourceName": null
}
],
"message": "SyntaxError: Unexpected token 'export'"
}
]
}

Collapse
ganapathiraju profile image
ganapathiraju

Problem resolved. There was error in the lambda function.
Kindly ignore the query

Collapse
lucidoapps profile image
lucido-apps

Hi Nader,

Thank you for this post! Most of the tutorials and documentation I find about Lambda resolvers involve creating new queries and mutations to use the Lambdas. However, I think it would be very helpful to use Lambda resolvers on queries and mutations that are generated automatically from the GraphQL schema by Amplify.

For example, let's say I have a schema that defines a Person with a phoneNumber field. Amplify automatically generates getPerson, createPerson, etc. How can I create a lambda resolver that is triggered on createPerson (for example to validate the phoneNumber)?

I tried many different approaches but I keep getting errors, it seems that Amplify doesn't play well with adding to or modifying automatically generated resolvers.

I know I can perform phoneNumber validation inside the VTL resolver, but what if I want to use a Node.js validation library for more concise and thorough data validation for all my default mutations?

Thanks again,
Santiago

Collapse
mjmaix profile image
MJ Abadilla

"currencyfunction-${env}" nice trick. Are there more variables available inside schema.graphql? Like project name?

Collapse
imewish profile image
Vishnu Prassad

Hi @dabit3 ,

Nice article. I was able to set up my first Appsync API today. How can we return a custom error messages in a mutation call with lambda resolver?

Collapse
plukkemx profile image
Plukke

Is it possible connect an existing DynamoDB table to noSQL storage for using it with functions?

Collapse
indatawetrust profile image
Ahmet Simsek

Thanks