Create a serverless GraphQL API with AWS AppSync and MongoDB
Tags: graphql, aws, serverless, mongodb
Published: 2020-09-25 17:06:31

> This tutorial will give you an introduction to GraphQL and how you can integrate external data sources like MongoDB using AWS AppSync and AWS Lambda, it is easy to follow even if you aren't familiar with AWS

# What is GraphQL?

GraphQL is a new standard for API, you can do queries at the API level and retrieve only the data you need, which is a perfect fit for mobile applications where bandwidth is limited and you want to retrieve all the data you need (and nothing more) in a single network request.

---

# How does GraphQL compares with REST API then?

Using traditional REST APIs, say when we want to get the title, year released and director of a particular movie, we would need to send a HTTP GET request to the /movies endpoint. Here is the response we get from the server:
```
"title": "Star Wars: Episode IV - A New Hope",
"year": 1977.0,
"rated": "PG",
"runtime": 121.0,
"countries": ["USA"],
"genres": ["Action", "Adventure", "Fantasy"],
"director": "George Lucas",
```
Hmm, but we just want the movie's title, year and director, and the server is sending us a bunch of information we don't need. And how about if we want to retrieve the information about the director? We would need to send another GET request to the /director endpoint again...

***

## Can we do better?

With GraphQL, we can select what information we need when we make a request to the server (a.k.a. query), so let's say this time we want the title of a movie, its actors and metacritic rating, then we will send a query like this:
```
query{
    getMovieByTitle(title: 'Star Wars: Episode IV - A New Hope"') {
        title
        actors
        metacritic
    }
}
```

And here is the server response:
```
{
    "data": {
        "Movies": [
            {
                "title": "Star Wars: Episode IV - A New Hope",
                "actors": ["Mark Hamill", "Harrison Ford", "Carrie Fisher", "Peter Cushing"],
                "metacritic": 92
            }
        ]
    }
}
```

Much better, isn't it?

---

# Creating our first GraphQL API

## Account setups

For this tutorial, we will be using AWS and MongoDB Atlas, and we will need to create free-tier accounts for both of them. The good news is, all services we use in this tutorial are covered by the free-tier, so we don't have to pay for even a penny.

### Setting up our AWS Account

1. Go to the [AWS Console](https://portal.aws.amazon.com/billing/signup#/start).<br>
(<strong>Note: a credit card is needed for registration, but we won't incur any charge so no worries</strong>)
2. Now we have our root account set up, but it is generally a bad idea to do stuffs with that, so we will create a new user to work with.

    > Follow [this guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started_create-admin-group.html) to create a new IAM user in the console

3. Once you see the AWS Console, you're good to go! ![AWS Console](https://dev-to-uploads.s3.amazonaws.com/i/rohio1ui1zyimk3m9vvp.png)

### Setting up our MongoDB Atlas Account

1. Register an account at the [MongoDB Atlas Website](https://www.mongodb.com/try)
2. Create an organization ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/a7dfkyi7k3kwom7i3s91.png)
3. Enter a name for the organization, choose **MongoDB Atlas**, click 'Next' and create the organization
4. Click 'New Project'
5. Give this project a name, we will use 'graphql-demo' here, click 'Next' and create the project.
6. Now we can build our cluster! Click the green 'Create Cluster' button
7. Free cluster is fine for us, and for the deployment option we will be using the following settings:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mas0z39ldj1odnwar6l6.png)
Ideally, we will want our cluster in the same AWS region we will be using (us-west-2 for this case, and if you want to use another region, [just make sure AWS AppSync is available in that region](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/))
8. Hit 'Create Cluster', and our cluster will be ready in few minutes. Meanwhile, we will set up our development environment in AWS

## Setting up our development environment

We will be using [Cloud9](https://aws.amazon.com/cloud9/?nc2=h_ql_prod_dt_c9) as our development machine, so it doesn't matter whether we are using Mac or Windows.
> You can also work on your local machine using VSCode or other IDE as you like, but you may have to install some additional tools required for this tutorial, which Cloud9 has them pre-packaged already.

1. In the AWS Console, click 'Service' and search for <strong>Cloud9</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lm1xx155ymg4wj5fp7hh.png)
2. Let's change our region to <strong>'Oregon'</strong> (us-west-2)

    > You can find the region selector near your account number on the upper right corner

3. Click 'Create Environment'
4. Let's call our development machine <strong>'AppSync-Cloud9'</strong> and click 'Next Step'
5. For <strong>Platform</strong>, select <strong>Ubuntu Server 18.04 LTS</strong> and click 'Create Environment'

Now let's wait for our IDE to load, and once it is ready we can see the welcome screen with a terminal below it. In the terminal, enter the following command:
```
git clone https://github.com/gai6948/appsync-mongodb-tutorial.git && cd appsync-mongodb-tutorial
```
We have just downloaded the code for this tutorial and we will set up our database first for now.

## Setting up our MongoDB database

### Create our database and collection

1. Go back to our MongoDB Atlas console, select the project we created previously
2. On our cluster, select <strong>'Collections'</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/q15gtze1pd4ez9bwy5l4.png)
3. Select <strong>'Add My Own Data'</strong>
4. Enter <strong>'appsync-db'</strong> for database name, and <strong>movies</strong> for collection name, and click 'Create'

### Configuring Database Access

We will need to create a database user for our Lambda function (to be created later) to talk to the database.

1. Click <strong>Database Access</strong> under the section of <strong>Security</strong>
2. Click 'Create Database User'
3. Enter a username and password, and leave other settings as default and create the user

    > Normally, we should limit the user access to just our <em>movies</em> collection, but for simplicity we will leave the settings as default in our tutorial

4. We will also need to configure the IP whitelist for our Lambda, click <strong>Network Access</strong> and <strong>Add IP Address</strong>
5. Select <strong>'Allow access from anywhere'</strong> and <strong>Confirm</strong>


#### Installing MongoDB tools on our Cloud9 Environment

To import data to our MongoDB Atlas Cluster, we will need to install the mongo tools on our Cloud9 Instance first.

Switch back to the Cloud9 IDE, and execute the following command in the terminal:
```
sudo apt-get install gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E52529D4
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.2.list
sudo apt-get update
sudo apt-get install -y mongocli
sudo apt-get install mongo-tools
```
After executing the above command, run `mongocli --version` to verify the installation

#### Import our dataset

We will use a trimmed down version of the IMDB movie database as our sample dataset, you can inspect the raw data [here](https://github.com/gai6948/appsync-mongodb-tutorial/blob/master/movieDetails.json) 

1. Go back to the <strong>'Cluster'</strong> Tab
2. Click on the <strong>'Command Line Tools'</strong> tab after the database is created
3. Look for the section <strong>'Data Import and Export Tools'</strong>, and copy the <strong>mongoimport</strong> command
4. Now go back to our <strong>Cloud9</strong> console, and paste the command into the terminal, but let us change some parameters first:

* Change <strong>&lt;PASSWORD&gt;</strong> to the database user password we just created
* Change <strong>&lt;DATABASE&gt;</strong> to <strong>appsync-db</strong>
* Change <strong>&lt;COLLECTION&gt;</strong> to <strong>movies</strong>
* Change <strong>&lt;FILETYPE&gt;</strong> to <strong>json</strong>
* Change <strong>&lt;FILENAME&gt;</strong> to <strong>movieDetails.json</strong>

We should see <strong>'imported 100 documents'</strong> in the command output

## Create our GraphQL Schema

Before we start working on our code, let us define our GraphQL Schema first, the schema acts as a contract between the client and the server on the data types client can request.

### Create our AppSync API

[AWS AppSync](https://aws.amazon.com/appsync/?c=m&sec=srv) is a managed GraphQL service that let us define a GraphQL schema and leverage various data sources such as [Lambda](https://aws.amazon.com/lambda/?nc2=h_ql_prod_cp_lbd), [DynamoDB](https://aws.amazon.com/dynamodb/?nc2=h_ql_prod_db_ddb) and much more. It is fully serverless and we don't have to worry about servers at all. The cool thing about AppSync is you can define your schema and let AppSync creates a DynamoDB table and ElasticSearch cluster for you, without you having to set them up manually. But we will be using Lambda as our data source in this tutorial, so we can connect to our MongoDB database. 

1. Open the [AppSync Console](https://us-west-2.console.aws.amazon.com/appsync/home?region=us-west-2#/) in a new tab.
2. Click <strong>'Create API'</strong>
3. Select <strong>'Start from scratch'</strong> and click the start at the top
4. Click <strong>'Create'</strong>, you can also give the API a name but that's optional
5. Once the API is created, click <strong>'Edit Schema'</strong>

### Work on our GraphQL Schema

A GraphQL Schema consists of three types: <em>query</em>, <em>mutation</em> and <em>subscription</em>. <strong>Query</strong> allow us to specify what data we want, <strong>mutation</strong> lets us insert or update new data and get back the result as confirmation, while <strong>subscription</strong> allows us to listen for data updates and be notified when they happen. We will work with queries in this tutorial, and add mutations later. You can learn more about GraphQL Schema [here](https://graphql.org/learn/schema/)

1. Remove everything in the schema first, we won't need them
2. Add the following code at the beginning of the schema, they are the root types that we allow, which is query for now.
    ```
    schema {
      query: Query
    }
    ```
3. Add the type definition for movies below the root definition:
    ```
    type Movie {
      actors: [Actor!]!
      director: String!
      genres: [String]
      id: ID!
      title: String!
      year: Int!
    }
    ```
    Here we define the data types for movies, <strong>!</strong> behind the data type means the field cannot be null, <strong>gnere: [String]</strong> means the field genres would return an array of genre, and <strong>[Actor]</strong> means we expect an array of Actor object to be returned, we will define the Actor object in a moment.

4. Add the type definition for actors below movies:
    ```
    type Actor {
        featured_in: [Movie!]
        name: String!
    }
    ```
    An actor can appear in multiple movies, so we define a list of <strong>movies</strong> for the field <strong>featured_in</strong>.
    Now we have our data types defined, we can define the queries and mutations now.
5. Add the query definition below:
    ```
    type Query {
        getMoviesByGenre(genre: String!): [Movie]
        getMoviesByTitle(title: String!): [Movie]
    }
    ```
    We will allow clients to query for movies using <strong>genre</strong> or <strong>title</strong> as argument, and return a list of matching movies. The completed schema should look like this:

    {% gist https://gist.github.com/gai6948/58e6a0d24948551bf1021e1f12e4e69e %}

We can also allow more query patterns like by director or year, and you can add them easily after learning how to write resolvers for queries and fields, which we will cover soon.

## Create our Lambda resolver function

The magic that we can get whatever data we want is made possible by resolvers, they gather the required data and pass them back to the client in the format defined. Resolvers can be defined at <strong>query</strong> level or <strong>field</strong> level. For example we can create a resolver for our `getMoviesByGenre` query, which can <strong>'resolve'</strong> all or part the fields defined in the `[Movie]` type. We can also set resolvers at field level, like I can define a resolver `getYearInMovie` which only <strong>'resolves'</strong> the field `year`. In this tutorial, we will have 2 query level resolvers to handle `getMoviesByGenre` and `getMoviesByTitle` respectively, and a field level resolver to resolve the `[Actor]` field.

In AWS AppSync, there are different types of resolvers:

    * [AWS Lambda](https://aws.amazon.com/lambda/?nc2=h_ql_prod_fs_lbd), which is <strong>serverless</strong> function that only run when being invoked

    * [DynamoDB](https://aws.amazon.com/dynamodb/?nc2=h_ql_prod_fs_ddb), which is a serverless NoSQL database in AWS

    * [Aurora Serverless](https://aws.amazon.com/rds/aurora/?nc2=h_ql_prod_db_aa), which is also a serverless database but it is relational and compatible with MySQL or PostgresQL

    * [Amazon ElasticSearch](https://aws.amazon.com/elasticsearch-service/?nc2=h_ql_prod_an_es), which lets you implement text-based searching for your application

    * <strong>HTTP Endpoint</strong>, which lets you call a REST API to other data sources

We will create a Lambda function now to act as a resolver for our GraphQL Schema, and we will use [AWS SAM](https://aws.amazon.com/serverless/sam/) for that.

### Create our Lambda function via SAM template

1. Let us go back to our Cloud9 IDE and run `sam init` on the terminal

2. Choose <strong>AWS Quickstart Template</strong> for template source, <strong>nodejs12.x</strong> for runtime, we will name our project <strong>appsync-demo</strong> and we will choose <strong>Hello World Example</strong> as a starting point

3. Once finished, a new directory will be created with the required files like this:
    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zp5h1dw7cpjhez9fg9x3.png)
    We will define our code in `hello-world/app.js`, and `template.yaml` is where we specify properties of our Lambda function

4. Double click the `template.yaml` file to edit it, and replace the whole content with the following one:

    {% gist https://gist.github.com/gai6948/352ce3d23ce08be73a62a410ceda3c14 %}

    Here is what we changed: 
    * Set the function timeout to <strong>30 seconds</strong> so that would allow enough time for our lambda to retrieve data from our MongoDB cluster.
    * Removed the default API Gateway which is not required in our scenario, our function will be invoked by AWS AppSync.
    * Added an environment variables <strong>MONGODB_URI</strong> which will be our connection string to MongoDB Atlas, to get the connection string please follow the following steps:
        1. Go back to the home page of your MongoDB Atlas Cluster.
        2. Click the 'Connect' Label
        ![Atlas Connect](https://dev-to-uploads.s3.amazonaws.com/i/79a4wihv04oyw2l28kei.png)
        3. Select 'Connect your application'
        4. Copy the connection string and paste it as the value of <strong>MONGODB_URI</strong>

        > *Remember to change them according to the database and password you defined in MongoDB Atlas (*without enclosing &lt;&gt;*)*

5. Press <strong>CMD+S</strong> (Mac) or <strong>CTL+S</strong>(Windows) to save the template file

### Work with our Lambda code

1. Double click the `app.js` file in the `hello-world` directory

2. Paste the following code first, we will go through them by section. Also in case you did not name your MongoDB Atlas database as `'appsync-db'` and collection as `movies`, you have to change them in the code to match your database/collection name.

    {% gist https://gist.github.com/gai6948/06d10b9915d79dd8cdba568a7fe43428 %}

    In our code, we first imported the mongodb driver for connecting to our database, then for our async lambda handler (<strong>Yes it is async so we supply a callback so whenever we finish execution we pass in either error message if any, or the result</strong>), we initialize connection to our Mongo database using the URI you just set as environment variable in the template.

    This function will serve as multiple resolvers depending on which query pattern our client use (by movie title or genre in our case). The resolver for actor is to resolve nested array of movie objects representing the movies that an actor had featured in. We switch between different functions depending on the field supplied as event when AppSync resolves our GraphQL queries. You can consider event is something like arguments in a function. (**We will set how AppSync resolves fields by passing different values into event later so no worries if it makes no sense to you now**)

    For each resolver function, we extract the argument from event and use it as query parameter in our MongoDB queries. Once we get the results from our database we will append each result into an array of `Movies` object which is returned to AppSync.

    > <strong>It is important that we enclose the async database query inside a try-catch block so we can return errors if there is any. </strong>

3. Save the file

4. Run the following command <strong>one-by-one</strong> to initialize npm and install the MongoDB driver we need for our code.
    ```
cd ~/environment/appsync-demo/hello-world
npm init
npm install -y mongodb
    ```

### Deploy our code to AWS Lambda

We can now build our function and deploy it to Lambda using <strong>SAM CLI</strong>.

1. Let's go back to where our `template.yaml` is defined and build the function code with our dependencies
    ```
cd ..
sam build
    ```

2. (`Optional`) We can test our Lambda function locally before pushing to production, using SAM's local testing feature. If you want to test the function, replace the `events/event.json` file with the following one:
{% gist https://gist.github.com/gai6948/98196c1585ea85162a03c76b603eeeb4 %}

    This event simulates that a client query for movie by title "Wild Wild Test". For our GraphQL API the actual event is created by AppSync once we define how AppSync resolves the fields, we will do that later.

    Now <strong>execute this command</strong> to test our function using our newly modified event file

    ```
sam local invoke -e events/event.json
    ```

    If your function runs correctly it should produce this result:

    ```
[{"id":"5f3149e44ea473d321c2a796","title":"Wild Wild West","year":1999,"director":"Barry Sonnenfeld","genres":["Action","Western","Comedy"],"actors":[{"name":"Will Smith"},{"name":"Kevin Kline"},{"name":"Kenneth Branagh"},{"name":"Salma Hayek"}]}]
    ```

    If your function goes into error go check if you have provided a correct connection URI to MongoDB.
    >  Every time you modify your code remember to save it and run `sam build` again before invoking it

3. Now we will create an [S3](https://aws.amazon.com/s3/) bucket, and package our code to it so SAM can deploy the code for us

    Execute the following command on Cloud9 terminal:
    ```
SAM_BUCKET=sam-staging-bucket-$(uuidgen)
aws s3 mb s3://$SAM_BUCKET --region us-west-2
sam package --s3-bucket $SAM_BUCKET --output-template-file output.yaml
    ```

4. Finally we can deploy our code to Lambda by running `sam deploy --template-file output.yaml --stack-name appsync-demo --capabilities CAPABILITY_IAM`

5. If we go to the [Lambda Console](https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/functions), we can see our function is created.
    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/23ynkpx67guh8pu5dbhb.png)

## Add our Lambda resolver to our AppSync Schema

Now we have our Lambda function ready, it's time for us to tell AppSync how to <strong>interpret GraphQL queries or mutations</strong> and use our Lambda to resolve the fields for the client. The way it works is by passing different values in the <strong>event</strong> to Lambda using [VTL Templates](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-overview.html). You can tell the template to query against *DynamoDB tables, Aurora cluster, make REST API calls*, etc. <strong>But now we will set the template to transform client query into event for Lambda</strong>

### Add our Lambda function as data source for AppSync

1. Go back to the [AppSync Console](https://us-west-2.console.aws.amazon.com/appsync/home?region=us-west-2#/apis) and find the API you created earlier

2. Select <strong>'Data Sources'</strong> on the left and click <strong>'Create Data Source'</strong>

3. Create a <strong>Lambda data source</strong> like this:
    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wku8ixjc4fu9n4hnhng1.png)

4. Hit <strong>'Create'</strong>

### Setting up the resolvers

1. Go to <strong>Schema</strong>

2. You should see the <strong>'Query'</strong> section for <strong>'Resolvers'</strong> 
    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/swh487zcd1r8jd1p6bdz.png)

3. Click <strong>'Attach'</strong> for `getMoviesByGenre`

4. Select our only data source and <strong>enable</strong> both *Request and Response mapping template*

5. Replace the default <strong>request</strong> mapping template with this one:

    ```
    {
      "version" : "2017-02-28",
      "operation": "Invoke",
      "payload": {
      	"field": "getMoviesByGenre",
        "arguments": $util.toJson($context.args)
      }
   }
    ```

    On the `operation` field we specify <strong>'Invoke'</strong>, which means calling a *Lambda Function*.

    The `payload` field lets us specify what values to put in the <strong>event</strong> for our Lambda function, remember we set a switch on `field`? We will tell AppSync to pass in the value <strong>'getMoviesByGenre'</strong> to our Lambda if the query is taking genre as argument, and for the actual argument we will use the default utility function to transform the format that is readable by our Lambda function.

    > Remember to click <strong>Save Resolver</strong> at the top

6. Attach a resolver for `getMoviesByTitle` this time

    The steps are the same as before, but for the <strong>request</strong> mapping template we will be using this:
    ```
    {
      "version" : "2017-02-28",
      "operation": "Invoke",
      "payload": {
  	"field": "getMoviesByTitle",
        "arguments": $util.toJson($context.args)
      }
    }
    ```

7. Now we have two <strong>root-level</strong> resolvers set up. In the AppSync console we can set resolvers at field-level, but for most of the fields we don't have to do so because <strong>if the parent resolver returns an object with keys matching the child field names, then most GraphQL engines (like AppSync) will use the fields returned from parents unless you explicitly override.</strong>

8. We only have 1 <strong>field-level</strong> resolver to set, and that is `featured_in` for the `Actor` object

9. Do the same steps as how you set the resolver for the parents, but the <strong>request</strong> mapping template will be this:
    ```
    {
      "version" : "2017-02-28",
      "operation": "Invoke",
      "payload": {
  	"field": "getMoviesByActor",
        "arguments": $util.toJson($context.source)
      }
    }
    ```

    Our GraphQL API is ready for now

## Playing around with our GraphQL API

    We can now go to the <strong>Queries</strong> tab and play around with our API. Click the <strong>+</strong> icon near `Query` to add a query, and select what fields you want for your query. You can also type your query in the IDE if you prefer it.

    The result should be something like this:

    ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k7hg5yv3iikn5tq33i4a.png)

    You may notice we can go a few layers deep and answer questions like given a movie title, find the movies that its actors have featured in. This is not easy to achieve using traditional REST API. Also we have the freedom to choose how much data we need, no more no less, and this is the power of GraphQL

# What's next?

🎉🎉🎉 <strong>Hurray, we now created our first GraphQL API! </strong>

I have only showed you a subset of <strong>AppSync</strong>'s capabilities, in fact, combining AppSync with [AWS Amplify](https://aws.amazon.com/amplify/) can give you the ability to create a full-stack web/mobile app in days.

I suggest you read [Nader Dabit's tutorial](https://aws.amazon.com/blogs/mobile/building-scalable-graphql-apis-on-aws-with-cdk-and-aws-appsync/) on building a full-stack app with <strong>AWS AppSync and TypeScript</strong>, and also [Traversy Media's Youtube Tutorial](https://www.youtube.com/watch?v=w0p7ywfHesw&t=7378s) on building a full-stack app using <strong>AWS Amplify and AWS AppSync.</strong>

Stay tuned for more content 🔥🔥🔥🔥