Go Forth and AppSync!

Mat Warger Amazon Web Services, AWS, JavaScript, Technology Snapshot Leave a Comment

In a previous post, we discussed the basics of GraphQL and how it can be a great REST API alternative. In this one, we’ll see how AppSync can be more than just a great API alternative — it gives you a soft landing into the world of GraphQL.

Recall our Game API example? Let’s start with the basic type of a game. Follow along and we can implement a simple schema in AppSync together.

type Game {
  title: String!
  description: String
  rating: Int!
}

Types and Resources

Head over to AppSync and sign in. In the top right, click “Create API”.

In the next screen, change the name to something like “My Game API” and click “Create.”

You should now be on a screen that has your API name at the top, as well as a couple URLs. We don’t need to worry about any of this for now, because we’re just going to be taking a look at the console. Click on the Schema link on the left-side so we can see what we’re given to begin with.

You’ll see a bunch of commented stuff in the schema, so feel free to read that if you want. We’re not going to use it though, so you can delete it when you’re ready.

A Basic Type

Paste the following snippet in the text area. This should replace anything that was in the box.

type Game {
  id: ID!
  title: String!
  description: String
  rating: Int!
}
type Query {
  getGames: [Game]
}
schema {
  query: Query
}

This holds our basic Game type from the previous post (with an additional ID property), as well as a placeholder query to get an array of games. Don’t worry if this doesn’t make sense yet, we will go over this in more detail.

Next, take a look at the sidebar on the right side that lists the data types. You’ll see that the three sections we have for Game, Query, and Schema are listed there as well as the fields under each of them.

You may have noticed that our type is missing a property from before — the rating. Let’s add that. On a new line before the description field, create a rating of type Int!. Your Game type should now look like this:

type Game {
  id: ID!
  title: String!
  description: String
  rating: Int!
}

As was mentioned in the previous post about types, this defines two string fields, title and description (of which the title is non-nullable) and a rating field of type Int (which is also non-nullable). You can also see that after adding the field to the schema on the left, the field was adding under the Game type listing in the sidebar on the right. Hit the “Save” button in the bottom right so we can save our changes.

Resources

Now that we have our type, let’s create a resource. AppSync allows us to create resources automatically for the types in our schema (for more information, check out the docs). To begin, click “Create Resources” in the top right corner.

Under the heading for “Select a type,” choose Game, and take a look at the panel that is filled out for us. AppSync will provision a DynamoDB table for us and add a default index, as well as create Queries and Mutations for us and wire up all the resolvers. For now, just hit the “Create” button at the bottom to accept all the defaults. We’ll look at this in a little more detail later when we add some more to our schema.

You’ll see the status bar at the top letting you know that the table is being created. It will fill in the schema with the types, Queries, and Mutations that will allow us to manipulate our Game data. Let’s take a look at what it has given us. The first thing we’ll consider are the queries.

Look in the schema and find the type Query listing:

type Query {
 getGames: [Game]
 getGame(id: ID!): Game
 listGames(first: Int, after: String): GameConnection
}

Go ahead and delete the getGames placeholder query from before and hit the “Save” button. Your Query and Mutation types from before should now look like this:

type Mutation {
 createGame(input: CreateGameInput!): Game
 updateGame(input: UpdateGameInput!): Game
 deleteGame(input: DeleteGameInput!): Game
}
type Query {
 getGame(id: ID!): Game
 listGames(first: Int, after: String): GameConnection
}

Let’s explore these by testing them out in the Queries editor.

Queries and Mutations

On the left sidebar, select Queries from the list.

You should see the Queries window with a bunch of commented stuff. You can delete this when you’re ready to continue or leave it for later. We’re going to add our own Queries and Mutations as we explore manipulating data with GraphQL.

Take a quick look at the panel, and notice the little < Docs link on the right. Click that to take a look at the documentation for our API. As we mentioned in the previous post, the tooling that types provide with GraphQL is awesome, and this is a perfect example. Click around on the Query, Mutation, and Subscription fields to explore what our generated documentation looks like. When you’re done, press the X in the corner to close it.

A Basic Query

Let’s start by writing a Query. Type the following into the editor on the left.

query GetGames {
  listGames{
    items {
      title
      description
      rating
    }
  }
}

You could just paste it, but typing it will let you see how the types enable a nice feature — auto-completion of your queries! This is magical, and a great help when you’re getting something going and don’t want to have to have a separate reference. You can press Ctrl + Space at any time to get some help for what you can type.

What we’re doing here is straightforward. We tell AppSync that we’re creating a query that we name GetGames. This query will use the Query defined in our schema called listGames. This is defined in our schema like this:

listGames(first: Int, after: String): GameConnection

listGames takes two arguments, which we haven’t bothered to provide here. This is okay though, because if you remember from our earlier section on types — a type is only required if it’s marked with a !. The schema definition defined that the listGames query will return a GameConnection. A GameConnection is defined as follows:

type GameConnection {
 items: [Game]
 nextToken: String
}

In our query above, we say that within listGames, we want to get items. This is the same items referred to in our GameConnection type above. items is simply a list of type Game denoted by the [] around the type. At this point, we’re saying that for each Game we want the title, description, and rating.

If you press the big orange play button, you’ll notice that the results come back as null. This is because we haven’t added any Games to our list. Let’s fix that.

See Also:  AWS Lambda with Spring Boot

A Basic Mutation

Type the following mutation below the query we just created:

mutation CreateGame {
  createGame(input: {id: "123", title: "Rocket League", description: "Rocket car soccer!", rating: 10}) {
    title
    description
    rating
  }
}

This mutation is using what was defined in our schema as an input type. In our schema it’s defined as follows:

input CreateGameInput {
 id: ID!
 title: String!
 description: String
 rating: Int!
}

If you look at the createGame mutation, you can see that it takes an input.

createGame(input: CreateGameInput!): Game

We defined this input as an object with the different properties we want to use to create our Game. Then we define what properties we want returned after the creation is done, and that is our title, description, and rating just like before.

If you press the big orange play button, you’ll see that AppSync lets you pick which operation you want to execute. Go ahead and choose CreateGame and watch the result appear on the right.

Yes! You’ve just created your first Game with a mutation!

Note that if you try the operation again, you will get an error because the ID is the same. You can see the error return from the query with some helpful information about why the error occurred.

While we’re here, go ahead and try creating a few more games. Remember that you can always refer to the docs on the right if you want to know which fields are required or not.

A Basic Query Revisited

Once you have a few games created, go ahead and run the GetGames query again. You should see something like this:

{
  "data": {
    "listGames": {
      "items": [
        {
          "title": "Tomb Raider",
          "description": "Lara Croft raids tombs for fun",
          "rating": 9
        },
        {
          "title": "Doom",
          "description": null,
          "rating": 8
        },
        {
          "title": "Rocket League",
          "description": "Rocket car soccer!",
          "rating": 10
        }
      ]
    }
  }
}

Recall that our query looks like this.

query GetGames {
  listGames {
    items {
      title
      description
      rating
    }
  }
}

Notice how the nested structure is similar between the two? You can define which objects you want back by simply changing the query to define what data you want to return. Try removing the description and rating from the query:

query GetGames {
  listGames {
    items {
      title
    }
  }
}

Run it again and check out the results:

{
  "data": {
    "listGames": {
      "items": [
        {
          "title": "Tomb Raider"
        },
        {
          "title": "Doom"
        },
        {
          "title": "Rocket League"
        }
      ]
    }
  }
}

The object structure reflects the changes that you made to the query. This is powerful, as we didn’t have to change how the server or schema was structured, we only had to change what we ask for.

Querying Specifics

Before we move on, let’s take a look at one more Query. Recall that we have a query to get a specific game. Type the following below our other examples:

query GetGameById {
  getGame(id: "123") {
    title
    rating
  }
}

Like before, we’re defining a query with a name. This time, we pass in a parameter to the query, similar to how we passed the input type to our mutation previously. We’re specifying the id of the first game we created. Run this and see that you get these results:

{
  "data": {
    "getGame": {
      "title": "Rocket League",
      "rating": 10
    }
  }
}

Perfect. This is great, but it would be nice if we could pass in the ID we want to our query instead of hard-coding the ID. We can accomplish this with query variables.

At the bottom of the screen, you should see a label called QUERY VARIABLES. Click on it to bring up the variables pane.

It should look like this.

Type or paste the following into that box:

{
  "id": "123"
}

Here we’re declaring the id variable as “123”. Now let’s use it in our GetGameById query. Update the query to be as follows:

query GetGameById($id: ID!) {
  getGame(id: $id) {
    title
    rating
  }
}

Here we’re declaring that our named query GetGameById can take an argument called $id of type ID!. We then replaced our hard-coded “123” in the getGame declaration to use the $id variable passed to the query. This allows our query to be flexible and accept its inputs as passed-in arguments.

Try running this query. You should get the same results as before. Update the id variable in the Query Variables pane and see what other games you can query.

Nesting Types

Our game type is fine the way it is, but one rating is a little sad. What if we wanted to allow each game to have multiple reviews associated with it? We could trust all the reviewers to decide how our games should be rated.

Let’s add some reviews to our schema.

Game Reviews

Click on the Schema heading on the left to get back to the Schema editor. Below the Game type, add the following:

type Review {
  id: ID!
  # The name of the reviewer
  author: String
  rating: Int!
  gameId: ID!
}

Here we’re adding a Review type with an id, the name of the author of the review, the rating like we had before, and the gameId to relate this Review to a Game. The # allows us to add comments to our schema that we can see in the docs to annotate specific information.

For the Game type, update it to the following:

type Game {
  id: ID!
  title: String!
  description: String
  rating: Int
   @deprecated(reason: "Using Review type instead.")
  reviews: [Review]
}

We evolved our API slightly by adding reviews and deprecating the rating field. We removed the ! to not required it any longer, and added a @deprecated annotation to note the reason why. This provides a good experience to users of the API as the clients will have the ability to update before they have a field suddenly removed. Press the Save button and we can move on to create the Review resource.

Click the "Create Resources" button in the top right.

Select Review from the drop-down and update the table pane to look like the following:

This is fairly similar to before, but we’re adding an index to the table that’s created so we can query for reviews based on their gameId. Click the "Create" button at the bottom and watch AppSync do all the work!

Creating Reviews

Once AppSync is finished, click over to the Queries editor again and let’s make some reviews!

One thing to note before we continue is the changes we made to the types. Open the docs panel as we did before and take a look at the changes we made to the types. If you check out the Review type, you’ll see the comment we added describing our author field.

If you look at the Game type, you’ll see that the rating field is now hidden behind the button to show the deprecated fields.

Click if you dare…

You can also hover over any of the fields to get the same information. It makes the Queries pane very helpful and easy to work with.

See Also:  How an SMS Application Took a Job (i.e. Task)

Below all the things we already have, add the following:

mutation CreateReview {
  createReview(input: {id: "1", author: "1337Gamer", rating: 10, gameId: "123"}) {
    id
    author
    rating
    gameId
  }
}

This will create a review for the game Rocket League. Just like before, take a minute to add a couple and practice playing with the parameters and returned values from the created reviews. You can even add them to the Query Variables if you’re feeling adventurous. 😉

Now take a look at the GetGames query we had previously.

query GetGames {
  listGames {
    items {
      title
      description
      rating
    }
  }
}

In AppSync, you’ll notice that there’s a yellow squiggly beneath the rating field. Hover over it to see the deprecated note.

It says to use the Review type, so let’s do that. Update the GetGames query to the following:

query GetGames {
  listGames {
    items {
      title
      description
      reviews {
        author
        rating
      }
    }
  }
}

Now that this is done, run the query and…

You’re gonna have a bad time…

There aren’t any reviews. This is because the Game type doesn’t know how to get the reviews for itself. Let’s look into how to do that next by adding our own Resolver for our game reviews.

Resolvers

Head back to the Schema so we can take a look at what we have so far. If you scroll down to the Mutation and Query sections, you should see something like this:

Click on the resolver link for the first Query: getGame.

Here you will see the Edit Resolver screen. This is how AppSync knows to get a game for a specific ID based on the query.

In the previous post, we talked about how Resolvers determine what each user-defined type is responsible for retrieving or handling. In AppSync, these take the form of request and response mapping templates. These are written in Apache Velocity Template language (more info here). There are lots of resources online and the docs are plentiful, so we won’t go into too much detail in this post. AppSync provides helpful templates for many common use-cases, so let’s take a look at how to use one to get the reviews for a game.

Head back to the Schema page and scroll to look at the Game type on the right.

You’ll see that all of the fields are scalars except for one: reviews. This is the field that we saw as null in our Query results earlier, and it’s because there wasn’t a resolver attached that told AppSync how to retrieve the list of reviews for a game. Click Attach next to the reviews entry.

You will be met with the “Create New Resolver” screen. Since we’re going to query for reviews, then select "ReviewTable" from the data source drop-down. AppSync will provide you with some default templates, but we’re going to change them to suit our needs. For the request mapping template, select “Simple query” from the drop-down.

AppSync will fill out the box with a sample template for retrieving an item by ID. However, we want to get a review based on the ID of the game that is part of the parent query. Remember our query?

Each of the items is a Game, and within each Game we have the reviews that we want. This Game is the parent, and the reviews are the children in this scenario. The parent within a resolver is considered the source and for our purposes, that’s what we want to use.

Replace the contents of the request mapping template with the following:

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "query" : {
        ## Provide a query expression. **
        "expression": "gameId = :id",
        "expressionValues" : {
            ":id" : $util.dynamodb.toDynamoDBJson($ctx.source.id)
        }
    },
    "index": "gameId-index"
}

There are a couple things to note here. First, the expression we’re using to find our review is made to look for the gameId, which is what we defined as the property in our table that will hold the ID of the game the review belongs to.

Second, the :id value is being set to $ctx.source.id because we want to use the ID of the source (or parent) of the item that this resolver is for. This will provide us with the ID of the game.

Finally, we’ve noted the “index” name that we created earlier, which is what DynamoDB can use to query the table for all the reviews we want to find by gameId.

The response mapping template is simpler. Since we’re just querying for a list, select the template “Return a list of results” from the drop-down on the right of the response mapping pane.

Press the “Save” button and head back to the Queries page.

Nested Results

Try our GetGames query again… and voila! You should see a list of our Games with the reviews for Rocket League listed out beneath the Game listing. The other games don’t have reviews yet, so they come back as empty lists. Try adding reviews for these games, or try changing the query to pull back different sets of results.

This approach is very flexible, and hopefully provides a window for you into why GraphQL is so powerful. The great part is that as your query changes, the resolvers and database logic all stay the same, so there’s no context switching once the initial pieces are wired up. Try adding your own types to different parts, maybe the Games could have comments? Maybe the comments can have flags to say whether people thought they were helpful?

Just the Beginning

I hope this gives you a good start to writing your own AppSync API.

We’ve only scratched the surface of what’s possible with AppSync so far. We added resolvers that use DynamoDB, but you can use ElasticSearch or Lambda as well to provide resolvers that fetch data from many different sources and provide the unified API layer hinted to in the previous post.

For those of you who have existing tables — you can also create a schema from an existing DynamoDB table, so you can get kicked off even faster with data you already have. There are also GraphQL features in general that we did not cover, like interfaces, enumeration types, Subscriptions, and pagination. We can look into this more as we approach the client side of things. More to come…

Thank you for reading!


About the Author
Mat Warger

Mat Warger

I'm a software consultant with Keyhole Software in Kansas City. I am a coder and life-long student, also enjoying technology, learning, cooking, and gaming. Recent projects have included React, Angular, and Java.


Share this Post

What Do You Think?