Creating a useful GraphQL server using AWS Amplify

Written by
Date published
August 19, 2020

Introduction

AWS Amplify brings together multiple AWS services (eg. S3, Lambda functions, DynamoDB etc.) by supplying a CLI, code libraries and premade UI (via their Amplify component library). These work together to generate the backend infrastructure required, as well as the communication and authentication between all the different AWS services. Amplify also writes all the specifications and config required to your project, so your infrastructure can be checked in to source control (infrastructure as code).

The tool can allow you to write a GraphQL schema, have the backend resolvers and database generated automatically, plus host everything on AWS, and a lot more.

Prerequisite:

  • AWS account

Everything covered in this blog is available on the free tier, though there may be charges if you go off-piste.

This blog will cover three major steps. First, setting up Amplify CLI to control the Amplify project via command line which will allow you to create, deploy and update an API and its backend infrastructure. Following that, an explanation of the Amplify-supplied basic “Todo” example, deploying it, and reviewing AWS resources in your account. Finally, creating a custom application schema will introduce more concepts not highlighted in the “Todo” sample, and a number of others not highlighted in the Amplify documentation (such as sorting, many-to-many relationships). These will allow you to create more usable schemas that can handle many requirements.

Set up AWS Amplify

First, follow the first page of this guide to set up AWS Amplify CLI. After that, you will be able to add Amplify features to the project and see them appear in the AWS dashboard.

Open the folder you want to contain your project in your terminal, and run the command amplify init. This will ask you some basic questions about your app, but it’s good to keep things as default for now. The CLI will do some basic set up of IAM roles and provisioning an S3 bucket to store your deployments. You can check this out by going to CloudFormation in your AWS console, and going to the “Resources” tab in the stack created by Amplify. This is specified by the file amplify/#current-cloud-backend/amplify-meta.json which is generated by Amplify.

Create a basic GraphQL API

In the terminal, use the command amplify add api to begin API creation. Choose GraphQL and select the default options (including “Single object with fields” at the “What best describes your project” stage). After the schema has been created, it should open in your default code editor.

type Todo @model {
    id: ID!
    name: String!
    description: String
}

As we can see, this is a schema relating to a Todo application. The @model directive is an AWS AppSync directive which creates a DynamoDB table for the type, which in turn generates queries and resolvers for the type.

To run your schema locally, run amplify mock api in your terminal. You can also run amplify mock if you are using other Amplify features and mock them all at once.

By accepting the default options, Amplify will:

  • Set up a local DynamoDB database
  • Create the backend queries needed to interact with the database (in the form of .vtl files under amplify/backend/api/[api-name]/resolvers/
  • Create mutations/queries/subscriptions in JavaScript under src/graphql that can be used from a front-end GraphQL client
  • Create a local AppSync endpoint and GraphiQL interface you can use to perform GraphQL queries

Open the GraphiQL interface in your browser (the address will be on the terminal at the line AppSync Mock endpoint is running at http://192.168.0.10:20002). You can see in the “Documentation Explorer” slide-out in GraphiQL that there are a full suite of queries, mutations and subscriptions that you can use relating to the GraphQL schema. All of these Queries, mutations and subscriptions actually work; you can use the createTodo mutation to create new todos, use any of the queries to return data, and use the subscriptions to watch for events. All of this was generated automatically without any backend or database coding.

Create a custom GraphQL schema

The default example is good as it shows you the power of using Amplify to create your API. As it only contains one type, it is not very useful for a more complex application. This example shows a slightly more complex GraphQL schema in order to create a GraphQL server that can be used by the demo application. With that in mind, you can add some more types. In this example, the types will allow the user to propose interesting topics to discuss with coworkers, which the coworkers can then comment on or tag with relevant keywords that can be used to group the topics together.

Replace the contents of schema.graphql with the code below:

type Topic @model {
  id: ID!
  title: String!
  description: String
  comments: [Comment] 
  tags: [Tag] 
}

type Tag @model {
  id: ID!
  tag: String
  topics: [Topic]
}

type Comment @model {
  id: ID!
  text: String
  topic: Topic
}

The schema, if the resolvers are created properly, will result in an API that fulfills the requirements. You can create topics, add comments, add tags, and get all the topics that are related to a specific tag. You can also go to the GraphiQL window and refresh to see the new schema in action.

One-to-many relationships

Add a topic by using the createTopic mutation below:

mutation {
  createTopic(input:{title:"Investigate Amplify"}) {
    id
  }
}

This creates a topic, and you can add a comment to the topic. A single Topic can have many comments. If you use the Documentation Explorer to navigate to the “createComment” mutation, you will notice that there is no way to link the comment you would create to a specific topic. This is because Amplify doesn’t know how to relate these two types together, so you have to use a custom Amplify GraphQL directive called @connection. This creates a link between two fields, and can be implemented per the following:

type Topic @model {
  id: ID!
  title: String!
  description: String
  comments: [Comment] @connection(keyName: "TopicComments")
  tags: [Tag]
}

type Tag @model {
  id: ID!
  tag: String
  topics: [Topic]
}

type Comment @model {
  id: ID!
  text: String
  topic: Topic @connection(keyName: "TopicComments")
}

If you save your updated schema and refresh GraphiQL, you will notice in the documentation for the createCommment mutation that there is a new field on the input for this type. This is the commentTopicId, a field generated by Amplify, that takes in a Topic id of the topic you created in order to link the comment and topic. Create a new comment using the code below:

mutation {
  createComment(input: {
    text: "Yeah, let's look into Amplify.",
    commentTopicId: "9500834d-186e-41ae-9ac5-ccef4b838afb"
  }) {
    id
    text
  }
}

You can the use the listTopics query to see your comment inside the topic:

GraphQL Query to list topics
GraphQL Query to list topics

Many-to-many relationships

To tag topics so you can see what types are being suggested, you can update the schema to create a new type that will contain the connection between tags and topics. A topic can have many tags, and a tag can be associated with many topics.

type Topic @model {
  id: ID!
  title: String!
  type: String!
  description: String
  comments: [Comment] @connection(keyName: "TopicComments")
  tags: [TopicTag] @connection(keyName: "byTopic", fields: ["id"])
}

type Tag @model {
  id: ID!
  tag: String
  topics: [TopicTag] @connection(keyName: "byTag", fields: ["id"])
}

type Comment @model {
  id: ID!
  text: String
  topic: Topic @connection(keyName: "TopicComments")
}

type TopicTag
  @model(queries: null)
  @key(name: "byTopic", fields: ["topicId", "tagId"])
  @key(name: "byTag", fields: ["tagId", "topicId"]) {
  id: ID!
  topicId: ID!
  tagId: ID!
  topic: Topic! @connection(fields: ["topicId"])
  tag: Tag! @connection(fields: ["tagId"])
}

The first thing you will notice is that we have a new type, TopicTag. This is the join between topics and tags, and allows many topics to relate to many tags. This new type will replace Tag in the Topic type, and will replace Topic in the Tag type. We can see that the Topic and Tag types that are being referenced are stored alongside the id of the topic/tag (topicID and tagId), which is necessary in Amplify to facilitate a link between the types.

There are a few custom Amplify directives that have been used as well. The @model(queries: null) prevents any queries to be generated for this type. This is because the TopicTag type is not meant to be queried directly, rather it is only to be accessed via a Topic or Tag. We then add new keys that can be accessed by connections in other types, the ‘byTopicandbyTag` keys which reference the topic/tag ids that are stored in the TopicTag type.

To add a tag, you need to do this in a separate mutation from the topic creation mutation, as you need to know the topic ID and tag ID before you can create a TopicTag.

GraphQL Query to create a tag
GraphQL Query to create a tag
GraphQL Query to create a link between a tag and a topic
GraphQL Query to create a link between a tag and a topic

This is not an ideal situation, as it’s kind of like creating a ‘join’ table in SQL, and also takes you a couple of steps away from our ideal schema. To retrieve the tags from a topic, you have to dig into a query to retrieve the tag. Also, you cannot tag a topic in the same mutation where you create the topic, since you need to know the ID before creating a TopicTag. This all has to be weighed against the time savings made by allowing Amplify to create database tables and backend resolvers automatically, and bear in mind that it also creates the front-end queries for you as well.

Sorting by latest

For the final part of the schema creation, retrieve the topics suggested by the date they were created. To do this, add a new key to the Topic type to tell DynamoDB to sort by it.

type Topic
  @model
  @key(
    name: "byCreatedAtDate"
    fields: ["type", "createdAt"]
    queryField: "getTopicsByCreatedAtDate"
  ) {
  id: ID!
  title: String!
  type: String!
  description: String
  comments: [Comment] @connection(keyName: "TopicComments")
  tags: [TopicTag] @connection(keyName: "byTopic", fields: ["id"])
  createdAt: AWSDateTime!
}

Also, you need to use the property createdAt , which is automatically added to all entries saved to the database. To access this, add the property at the end of the Topic type with the custom scalar AWSDateTime. More on these AWS specific scalars can be found here. In addition, add a field called type to the Topic type. This is a string that will contain the word “TOPIC” that we will have to set every time we create a new Topic. This field is required due to the way AppSync/DynamoDB handles creating keys and sorting.

The main thing you may notice, however, is the @key… directive added. This creates a new secondary index in the database that can be searched and sorted on, and you can use it to sort for that purpose. Give it a name, otherwise DynamoDB will try to use the fields supplied as a primary key. Since you created a secondary key (by giving it a name), you need to supply a queryField, which is the name of the query you will use in GraphiQL and the application code. Finally, notice that you have to supply two fields to the key. The first is the hash field, and the second is the sort field. In this instance, sort all the records that match the “type” supplied (“which will be “TOPIC”, as this is the only one you will supply) by their “createdAt” values.

To perform the sort and get data back, run a query similar to the one below:

GraphQL Query to retrieve topics sorted by date
GraphQL Query to retrieve topics sorted by date

You will have to use GraphiQL to either delete any topics you already made, or update them to add the string “TOPIC” to all the Topics in your database.

Deploy to AWS

You now have a very solid GraphQL schema that works well with AppSync and DynamoDB. Time to deploy this to your AWS account.

Amplify provides a very easy way to do this, simply by running the command amplify push. This updates the backend of your application to reflect the changes you have made in your dev environment (note that this does not update the remote DynamoDB tables with your dev data).

Once complete, you will have a GraphQL server hosted on AWS, the remote endpoint address will be shown in the terminal. Go into your AWS dashboard and see exactly how much work Amplify has done in getting your backend set up.

You can go to your Amplify dashboard to see the projects you have created. On the project homepage, click “Backend environments” and you will see “API” shown in categories added.

View of application Amplify dashboard
View of application Amplify dashboard

Clicking into this, you can see there is an AppSync API created for you, and you can click into to see it on AppSync. There are a bunch of DynamoDB tables now that you can also click into to see the DynamoDB dashboard.

View of DynamoDB tables created for your Amplify application
View of DynamoDB tables created for your Amplify application

You can also go to CloudFormation on your AWS dashboard to see how all this is orchestrated—there is a lot of configuration work that has been taken away from your plate.

Recap

AWS Amplify is a great way to create GraphQL APIs without having to write a backend or database, and without having to administer cloud services. The CLI is a powerful tool that makes spinning up servers and AWS services very straightforward.

This blog doesn’t cover the code libraries or the UI components that Amplify provides to allow you to quickly start using the services in your code. They do try very hard to make things easy for developers.

It’s not all plain sailing, though—you have to learn Amplify-specific ways of doing things (as seen when trying to provide many-to-many relationships and sorting results by date). These take you away from the ideal schema you would write if you had to write all the resolvers and create your own database. However, this is the trade-off between writing everything by hand and using tools like Amplify.

To learn more about Amplify, including the code libraries and UI components, visit the official docs.

Tags

See what Ranglers are writing about on our blog