Simple AWS GraphQL API Tutorial.

mim Armand

Easily, develop and deploy a secured GraphQL API using SLS, Appsync, Amplify & Cognito.

We will use the Serverless Framework, which will abstract away for us, underlying technomplexity, including, AWS Appsync, AWS Cognito, AWS CloudFormation, DynamoDB, etc,.. and a bit of AWS Amplify, as we will try to avoid using it in-place of SLS to keep things simple and manageable!

We will make a very practical API that everybody can use, it lets people add themselves to a database and also, optionally, indicate if they like me or not! they can also edit their response or delete their account, but, obviously, nobody should be able to see or edit ( or delete ) others data.

Update: part 2 (UI) of this series is published now here.

Prerequisites:

Serverless, now! The Serverless Framework is an open-source, awesome IaC scaffolding tool that you need to start using right away, period!
It essentially abstracts the complexity of Cloudformation templates, but it also has a ton of plugins, adding a bunch of features and functionalities!
This article is not about serverless, but if you don’t have it, simply run npm i -g serverless and you live longer!

You also need to have your AWS credentials set-up.

Let’s get to work!

0. If you want to jump to deployment (Step 7), simply run:

sls create --template-url https://github.com/mim-Armand/serverless-appsync-template
# then run:
npm i
# Then goto 7...
# but if you'd like to learn, please start from Step 1.

1. Creating the project shell structure:

sls create -t aws-nodejs -p testGraphqlApi

this will create a directory (testGraphqlApi) and a couple of files in it.
-t is for —-template and -p is for —-path. To learn more check the docs.
Feel free to delete the handler.js file as we are not using it here.

2. Installing the Serverless Appsync plugin:

sls plugin install -n serverless-appsync-plugin

This Plugin, by Siddharth Gupta, will let us define and use AWS Appsync infrastructure in our Serverless project, it also has a couple of neat features borrowed from AWS Amplify.

3. let’s define our GraphQL schema,

Create a file in the root of the project called schema.graphql. This file will define the structure of our GraphQL APIs, copy and paste the following code in it:

type UserType {
UserId: ID!
firstName: String
lastName: String
likesMe: Boolean
}

type Query @auth(rules: [{allow: owner}]){
getUser(UserId: ID!): UserType
}

type Mutation {
saveUser(firstName: String!, lastName: String!, likesMe: Boolean): UserType
deleteUser(UserId: ID!): UserType
}

type Schema {
query: Query
mutation: Mutation
}

Pretty, pretty… self-explanatory! we are defining a type (UserType), then we are defining GraphQL Query (to read data) and Mutation (to PUT data) types, finally we are using those to define the Schema.
There is one important thing though! that makes things much easier and needs to be explained, as it’s not a standard GraphQL SDL annotation, for that reason, we’ll create a fake step, just to put emphasis on how cool it is:

4. AWS Amplify Magic Sauce!

@auth(rules:[{allow: owner}]) in the previous step!
This is what we borrowed from AWS Amplify (one of its main value-propositions). it essentially abstracts away the complexity of defining common use-cases (like protecting a data-type or a method) in Vanilla GraphQL Schema Definition Language and mapping templates. Amplify, has a library that reads these annotations and converts them to fully described SDLs ( in Amplify, they’ll be injected directly into cloudformation templates, here, however, the Appsync plugin will translate those to be used with sls, which is much better!)
There are more directives, like @model, @versioned, or more complicated @auth definitions and more, so definitely check the Amplify documentation to learn about them. some of them, however, are not as compatible with the Server-less architectural concepts ( or even very well designed at all ) and may not have been implemented in the AppSync plugin yet (or forever), but, I’d suggest staying away from some of them anyway, as they may cause more headaches in the future by tying resources to code without explicit IaC definitions.

5. GraphQL request/response resolution mapping templates:

To keep things simple and maintainable, we will keep the logic about how GraphQL deals with each request type separately:
create the following files in a directory called mapping-templates:

  • common-response.vtl
$util.toJson($ctx.result)

The above common-response is saying, simply convert to JSON and return the context.result

  • deleteUser-req.vtl
{
"operation": "DeleteItem",
"key": {
"UserId": $util.dynamodb.toDynamoDBJson($ctx.identity.sub)
}
}
  • getUser-req.vtl
  • saveUser-req.vtl

As you can see, we created 3 request templates but one response, it’s because -in most cases, and here- the response we expect from each operation is similar, if we wanted to have a custom response, we could do that too.

6. Now let’s define our Infra:

This is the main reason we used Serverless framework, as, currently, it provides the best and cleanest option ( comparing to alternatives like Terraform and Amplify ) to handle multi-cloud Infra as Code. In the case of AWS, it abstracts the difficult to read/manage cloud-formation templates to beautiful and simple YAML files.
Edit the serverless.yml in the root of the project so it looks like below:

service: testGraphqlApi

provider:
name: aws
runtime: nodejs8.10
apiname: ${opt:apiname, 'testGraphqlApi-dev'}

plugins:
- serverless-appsync-plugin

# This is our Appsync infrastructure, consumed by the serverless-appsync-plugin:
custom:
accountId: { Ref: AWS::AccountId }
appSync:
name: ${self:provider.apiname}
region: ${self:provider.region}
authenticationType: AMAZON_COGNITO_USER_POOLS
userPoolConfig:
awsRegion: { Ref: AWS::Region }
defaultAction: ALLOW
userPoolId: { Ref: UserPool }
serviceRole: "AppSyncServiceRole"
dataSources:
- type: AMAZON_DYNAMODB
name: testGraphqlApiTableDS
config:
tableName: { Ref: testGraphqlApiTable }
serviceRoleArn: { Fn::GetAtt: [ DynamoDBRole, Arn ] }
mappingTemplates:
- dataSource: testGraphqlApiTableDS
type: Query
field: getUser
request: "getuser-request.vtl"
response: "common-response.vtl"
- dataSource: testGraphqlApiTableDS
type: Mutation
field: saveUser
request: "saveuser-request.vtl"
response: "common-response.vtl"
- dataSource: testGraphqlApiTableDS
type: Mutation
field: deleteUser
request: "deleteuser-request.vtl"
response: "common-response.vtl"

# These are our normal Serverless Framework IaC:
resources:
Resources:
# Amazon Cognito user pool
UserPool:
Type: "AWS::Cognito::UserPool"
Properties:
UserPoolName: ${self:provider.apiname}-user-pool

# An app client for the Amazon Cognito user pool
UserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
ClientName: ${self:provider.apiname}-appsync-client
GenerateSecret: false
UserPoolId: { Ref: UserPool }

# DynamoDB Table
testGraphqlApiTable:
Type: "AWS::DynamoDB::Table"
Properties:
TableName: ${self:provider.apiname}-kugelblitz-table
AttributeDefinitions:
- AttributeName: "UserId"
AttributeType: "S"
KeySchema:
- AttributeName: "UserId"
KeyType: "HASH"
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1

# IAM Policy to access Dynamo by the service
AppSyncDynamoDBPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
Path: /appsync/
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:DeleteItem
- dynamodb:UpdateItem
- dynamodb:Query
- dynamodb:Scan
Resource:
- { Fn::Join: [ '', [ { Fn::GetAtt: [ testGraphqlApiTable, Arn ] }, '/*' ] ] }
- { Fn::GetAtt: [ testGraphqlApiTable, Arn ] }

# IAM Role for implementing the AppSync / DynamoDB policy
DynamoDBRole:
Type: "AWS::IAM::Role"
DependsOn:
- AppSyncDynamoDBPolicy
Properties:
RoleName: ${self:provider.apiname}-appsync-dynamodb-role
ManagedPolicyArns:
- Ref: AppSyncDynamoDBPolicy
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- appsync.amazonaws.com

Believe it or not, this YAML file is a lot more human-friendly than its cloudformation JSON equivalent! but to make it ( even more! ) friendly, I spare a few comments here and there! You’re welcome! :/
Currently, you have four options for security ( API_KEY with forced expiration dates, AWS_IAM, OPENID_CONNECT, and, AMAZON_COGNITO_USER_POOLS which is the one we used here ), or you can use NONE and leave it open.

7. That was it! Just Deploy it!

Don’t worry, you can un-deploy it just as easily!

sls deploy

This will create all the resources required for this API, 12 of them to be precise! including, Dynamo tables, Cognito User-pools, IAM roles, and policies, AppSync APIs, and more.
And because Serverless uses cloudformation as its state backbone ( in contrary to Terraform ), it’s just as easy to take everything down. so after you are done testing and experimenting, just run sls remove to free up all the resources ( note that, by default, except if you explicitly ask it not to, it will remove everything, including the user-pools and databases and their contents, everything will be wiped clean like they never existed. )

Try out the API:

Before sls removeing your new shiny GraphQL API, you can try it out, even without an actual client. To do this you’d need:

1. Create a new user in your user-pool:

Since we used AMAZON_COGNITO_USER_POOLS for our API security, anybody who wants to call this API needs to be autheticated.
To do this, log in to AWS console and go to your Cognito user pools and select the correct pool ( testGraphslAPI-dev-user-pool )

create a new user by going to the Users and groups menu on the left and click on the Create user button.

Enter a user name and a password, a phone number ( remember the country code ) and an email address. also set them as verified by checking the checkboxes.

While you are here in Cognito, let’s also go ahead in the App clients menu on the left and take note of the App client id which we need to authenticate the user before making queries on its behalf.

2. Query the API:

In NodeJS, usually, we’d use GraphiQL to provide a GUI to test and diagnose GraphQL APIs during development ( or sometimes even after that, exposing it as an API specification/documentation tool! ).
In AWS Appsync, however, you can simply query your API on the console using the built-in tool. it also provides documentation for your API.

Go to the AWS APPsync service page, then select the appropriate API ( testGraphqlApi-dev ), then on the left menu select Queries and then select Login with User Pools.

Put the ClientId that you noted from previous steps and enter the Username and Password of the user you just created.

Because this is the first time this user logs in, you’ll need to change the Password, you can, however, for testing purposes, use the same Password you used when creating the user.

Here I went nutz and created a lot of arrows for no apparent reason! but after you are logged in, you can start making queries, I’ll put a few sample queries below, the thing also provides auto-completion.
One other cool/important thing to note is the Docs on the right of the screen which provides kind of an API specification. I’m not sure however if, currently, there is a way to expose or export this documentation.

To add a user you can use:

mutation PutUser{
saveUser(
firstName: "mim"
lastName: "Armand"
likesMe: false
){
UserId
}
}
UserId
}
}

The stuff in between the second set of curly brackets are what you would like to get back from the operation.
Also please note that a user can’t create more than one record in the database ( by design ), no matter how many time they run the above query because we are using the Cognito user UUID as the primary key in our users table. So you can also use the above query to get the userId of the currently logged in user.

Get User by ID:

You can use the ID you got from the above query used to create the user, to get the same user!

query GetUserById{
getUser(
UserId: "PUT-THE-ID-HERE-PLEASE!"
){
firstName
lastName
likesMe
}
}

Delete User By ID:

mutation DeleteUserById{
deleteUser(
UserId: "eecded7f-c064-4e31-83b8-bd64b73c35d4"
){
firstName
lastName
}
}

Another cool thing is that, even if we delete the user and recreate it, its UUID will be the same, since that’s coming from Cognito, unless we delete the user from Cognito as well.

Conclusion:

We demonstrated how easy it is to define and deploy a protected GraphQL endpoint with a user-base and everything else needed, The cool part is that, with this base we can create a lot more stuff, no matter how complicated they may be, we just add types and mapping templates and other security checks using Amplify annotations.
And we are not even limited to only AppSync and GraphQL! as I do in most of my projects, we can combine REST APIs and GraphQL, easily, just add other resources as you usually do use serverless, business as usual!

In another Story, especially if anyone’s interested? I can show you how easy it is to create a FrontEnd / UI to consume this API using AWS Amplify UI components, the right way! so you don’t need to worry about user management UI pages and authenticating the users and what not… if you are interested please let me know in the comments!

Part 2, about UI components is now available here!

Also, please don’t hesitate to connect with me on LinkedIn and/or Twitter, or leave a comment below, I’d love to have your feedback!

Best to you all,
- mim

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade