AWS AppSync with Lambda Data Sources

Mat Warger AWS, GraphQL, JavaScript, Technology Snapshot, Tutorial Leave a Comment

The power of GraphQL lies in its flexibility. That is especially the case regarding resolvers, where any local or remote data can be used to fulfill a GraphQL query or mutation.

In this post, I’m going to demo a quick example of what this looks like, and a couple gotchas that were apparent in working with Lambdas as a data source for AppSync. Let’s gooooo!

API Setup

I’m not going to get into the basics of setting up lambdas in this post, but I will point anyone over to the Serverless Stack tutorial. It is fantastic and I highly recommend getting familiar with it before moving to setup a Lambda data source with AppSync. For this walkthrough, I’m going to assume you have a basic serverless stack deployed so you have some Lambda functions available for use in an AppSync API. The one I am using for this demo is slightly modified to use Cognito for user authorization. This provides us with the user IDs needed to create our Game objects used in the API. You can find the code for it here. It has basic CRUD functionality for games and their associated reviews. We’re going to modify these to work with AppSync, while still maintaining the ability for our API to be used as a REST API if needed. This is the promise of GraphQL — adapting all data sources to be as one in a schema.

The API is used in this example app demonstrating the basics of AWS Amplify. It has some reviews that are based in AppSync, but we want the GraphQL API to resolve our games as well. We’ll use this opportunity to show how to adapt a serverless REST API to resolve GraphQL queries in AppSync, while maintaining the backward-compatibility of using it as a RESTful endpoint.

Starting with Types

I have found the easiest solution is to start with types. We already know what our Game type is going to look like, so let’s begin there. In your AppSync console, create the necessary type:

type GameItem {
  userId: ID!
  gameId: ID!
  content: String
  attachment: String
  createdAt: String
}

This is very basic, just for our needs at the moment. If you’re using the Notes API from the tutorial, or some other API you created, modify this to fit your needs.

Next, in the AppSync console, create the Query that we’ll use to fetch our GameItem list.

getGames: [GameItem]

Save the schema. That was easy. Now it’s time to wire up the resolver to our lambda data source.

Note: I’m assuming that you’ve already deployed the serverless API you’re using for this. If you’re following along, make sure to do that before continuing.

Data Sources

Click on Data Sources in the top left of the console to open the list. Click the New button in the top right to open the form and type a name into the box. I’m calling mine GamesFunction. We’ll start with the list function so we can return our list of GameItems.

The form should look something like this.

We’re going to let AppSync create a role for our function. Remember this, it will be important for later. Click Create and head back to the Schema section.

Resolver

The next step is to setup the resolver that will use our newly created lambda data source. Scroll down to the query section and click the button to Attach a resolver.

See Also:  Four Common Mistakes That Make Automated Testing More Difficult

Choose the GamesFunction from the dropdown list. AppSync will fill in a basic request and resolver mapping template for you. We’re going to need to change the request template to pass what we need to our function. If you recall, our list function looks like this:

This is retrieving all games for a given user. We need a way to pass in the userId. Through our serverless API, the authentication will automatically be handled for us and the event object will already contain everything we need. With AppSync, we have to adjust how we’re handling this.

As the request mapping template states, the payload will be passed in as the event. So let’s first update the payload to contain the identity of the user. We can get this from the identity context sub property.

{
    "version" : "2017-02-28",
    "operation": "Invoke",
    "payload": {
      "userId": $util.toJson($context.identity.sub)
    }
}

Next, let’s make a small alteration to our lambda function. We’re passing userId in and it will become a property on the event object. This will let us update our function to intelligently respond to how it’s being called, either through the GraphQL API or via a traditional REST endpoint with a JWT. If it’s called from our app it’s going to have the necessary headers in place for the auth, but more importantly it won’t have the userId property as a direct child of the event object. Take a look at what this lets us do:

We can now key off this difference, and adjust accordingly. There are two things of note in this case.

  • We’re using the userId to look up the list of games. In the ExpressAttributeValues, we’ll use the userId property if it’s directly on the event object— which will be the case if we’re calling it from our GraphQL API resolver.

Deploy this change and then head to the Query tab in AppSync. Assuming you have the authentication settings set to Cognito User Pools, login to the console with a user that has some games in the database. If this isn’t the case, take a minute to load up the app, and add a game or two using the example app.

Once you’re all set, try to run the following Query (or equivalent for your schema).

query GetGames {
getGames {
userId
gameId
content
attachment
createdAt
}
}

With any luck, it works! You should see the same list of games that your app is retrieving using the REST API — but you’re resolving it with GraphQL! You can adjust which properties are sent back by changing the fields requested in the query, just like you would expect.

Retrieving a Game

Let’s add one more resolver so we know we’re getting the hang of this. Head back to the Data Sources section and press the big orange New button to create another data source.

We’ll call this one GameFunction because we’re going to be retrieving a single game. Press Create.

We already have a type for our GameItem, so we just need to add a Query.

fetchGame(id: ID!): GameItem

Save the Schema. Head to the Query in the Resolvers panel and click Attach.

We need to modify the request mapping template like before. This time we’re passing in an argument for the Game’s ID, so we’ll handle that as well.

{
    "version" : "2017-02-28",
    "operation": "Invoke",
    "payload": {
      "userId": $util.toJson($context.identity.sub)
    }
}

Like the previous list resolver, we’re setting up the userId. Here, we’re also setting up a property called gameId. We’ll use this as a key in our lambda as well to provide some conditional logic.

See Also:  Improving Performance in React Applications

Head to the get function in your serverless project. It should currently look something like this:

And here it is after updates:

We essentially just want to use the success helper when we’re returning to the REST endpoint, and not use it otherwise. Also, there are some naive parts about this, but that’s okay for this demo. If you were doing many of these, you could probably extract this to a helper that would handle a key of some kind or even use a library like Middy to add some middleware logic.

Head to the Queries section and try it out!

query GetGame {
  fetchGame(id: "0d45f210-825c-11e8-a9a1-93d5167147ce") {
    gameId
    content
    attachment
  }
}

If you have the right logged-in user and id, you should see a result with our data resolved through the Lambda. Huzzah!

However, if you didn’t — or put in the wrong ID— you might be met with something like this:

{
  "data": {
    "fetchGame": null
  },
  "errors": [
    {
      "path": [
        "fetchGame",
        "gameId"
      ],
      "locations": null,
      "message": "Cannot return null for non-nullable type: 'ID' within parent 'GameItem' (/fetchGame/gameId)"
    }
  ]
}

This is ugly, and we already have a nice error message returning from our Lambda that is used with our RESTful endpoint. Let’s look at using that instead so we can provide a helpful message that let’s us know when a Game could not be found by the ID provided.

Head back to the schema and click back into the resolver for our fetchGamequery. Modify the response mapping template to the following:

#if( $context.result && $context.result.error )
    $utils.error($context.result.error)
#else
    $utils.toJson($context.result)
#end

We’re going to check that a result is returning and see if the error property is present. Recall our lambda function:

This will return the error instead of the item — so when we run our message with the wrong ID and don’t find an item, we get this:

{
  "data": {
    "fetchGame": null
  },
  "errors": [
    {
      "path": [
        "fetchGame"
      ],
      "data": null,
      "errorType": null,
      "errorInfo": null,
      "locations": [
        {
          "line": 12,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Item not found."
    }
  ]
}

This is a much better error, and can return an expected result in cases we want, like this one.

That’s it!

I hope this helped demystify the process of hooking up a lambda function to an API. This really helps with re-use and allows for backwards compatibility to ensure your previous apps can continue to work correctly.

Lambda is the gateway to the outside world from AppSync.

Sure, in this example we’re only calling to DynamoDB, but we could call to any other datasource, be it another database, another AWS service, or any external API we want! You get all the ease that AppSync provides, while being able to hook into any existing data that you need. Give it a try and let me know how it goes in the comments below!

Thank you for reading!

What Do You Think?