Build a GraphQL API on AWS with CDK, Python, AppSync, and DynamoDB(Part 2)

Subscribe to my newsletter and never miss my upcoming articles

Hi, Welcome to part 2 of this post series. In Part 1, we installed and created a CDK application alongside all was constructs needed to build this application.
We also went ahead to write some code to initialize and use a couple of constructs.


In this post, we will continue from where we left off in Part 1. Adding Resolvers. Resolvers are functions that connect fields of the graphQL schema to a data source. In our case, the data source is the DynamoDB we created.


Let's begin

Create Trainer Resolver

Create a folder in the root directory called resolver_functions.
Within that folder, create a text file called create_trainer and type in the following code

 {
                "version": "2017-02-28",
                "operation": "PutItem",
                "key": {
                    "id": { "S": "$util.autoId()" }
                },
                "attributeValues": {
                    "firstName": $util.dynamodb.toDynamoDBJson($ctx.args.firstName),
                    "lastName": $util.dynamodb.toDynamoDBJson($ctx.args.lastName),
                    "age": $util.dynamodb.toDynamoDBJson($ctx.args.age),
                    "specialty": $util.dynamodb.toDynamoDBJson($ctx.args.specialty)


                }
            }

This piece of code is a request mapping template that creates a trainer with an auto-generated ID,firstName,lastName, age, and specialty and saves to our DynamoDB table.
Take note of the operation:PutItem.


Now, let's attach this template to a resolver function.

In your cdk_trainer_stack.py, type in


with open(os.path.join(dirname, "../resolver_functions/create_trainer"), 'r') as file:
            create_trainer = file.read().replace('\n', '')  

create_trainers_resolver = CfnResolver(
            self, 'CreateTrainerMutationResolver',
            api_id=trainers_graphql_api.attr_api_id,
            type_name='Mutation',
            field_name='createTrainer',
            data_source_name=data_source.name,
            request_mapping_template=create_trainer,
            response_mapping_template="$util.toJson($ctx.result)"
        )

        create_trainers_resolver.add_depends_on(api_schema)

If you recall, in part 1, we had a createTrainer method in our mutation type.


So the type_name is Mutation, field_name is createTrainer, data_source_name is the name of the DB table, we pass in our template as a string to request_mapping_template and return a response($util.toJson($ctx.result)) through a response_mapping_template

Update Trainer Resolver

Create a file in the resolver_functions directory called update_trainer and type in the following code.

 {
                "version": "2017-02-28",
                "operation": "UpdateItem",
                "key":{
                    "id":$util.dynamodb.toDynamoDBJson($ctx.args.id)
                },
                "update":{

                "expression": "SET firstName = :firstName,lastName = :lastName, #ageField =:age,specialty = :specialty",

                "expressionNames": {
                "#ageField": "age"
                },
                "expressionValues": {
                ":firstName": $util.dynamodb.toDynamoDBJson($ctx.args.firstName),
                ":lastName": $util.dynamodb.toDynamoDBJson($ctx.args.lastName),
                ":age": $util.dynamodb.toDynamoDBJson($ctx.args.age),
                ":specialty": $util.dynamodb.toDynamoDBJson($ctx.args.specialty)
                }
                }


            }

The above template simply updates trainer information, based on their ID.
Here's how to attach that template to a resolver function

with open(os.path.join(dirname, "../resolver_functions/update_trainer"), 'r') as file:
            update_trainer = file.read().replace('\n', '')

update_trainers_resolver = CfnResolver(
            self,'UpdateMutationResolver',
            api_id=trainers_graphql_api.attr_api_id,
            type_name="Mutation",
            field_name="updateTrainers",
            data_source_name=data_source.name,
            request_mapping_template=update_trainer,
            response_mapping_template="$util.toJson($ctx.result)"
        )
        update_trainers_resolver.add_depends_on(api_schema)

Delete Trainer

Create a delete_trainer text file in resolver_functions folder and type in this piece of code.

{
                "version": "2017-02-28",
                "operation": "DeleteItem",
                "key": {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
                }
            }

This template deletes a trainer record based on their ID.
Here's it's corresponding resolver function.

        with open(os.path.join(dirname, "../resolver_functions/delete_trainer"), 'r') as file:
            delete_trainer = file.read().replace('\n', '') 

        delete_trainer_resolver = CfnResolver(
            self, 'DeleteMutationResolver',
            api_id=trainers_graphql_api.attr_api_id,
            type_name='Mutation',
            field_name='deleteTrainer',
            data_source_name=data_source.name,
            request_mapping_template=delete_trainer,
            response_mapping_template="$util.toJson($ctx.result)"
        )

        delete_trainer_resolver.add_depends_on(api_schema)

Get All Trainers

Create a text file called all_trainers in the resolver_functions folder and type in the following code.

{
                "version": "2017-02-28",
                "operation": "Scan",
                "limit": $util.defaultIfNull($ctx.args.limit, 20),
                "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
            }

This template gets all trainers as a paginated list in groups of 20.

 with open(os.path.join(dirname, "../resolver_functions/all_trainers"), 'r') as file:
            all_trainers = file.read().replace('\n', '')

 get_all_trainers_resolver = CfnResolver(
            self, 'GetAllQueryResolver',
            api_id=trainers_graphql_api.attr_api_id,
            type_name='Query',
            field_name='allTrainers',
            data_source_name=data_source.name,
            request_mapping_template=all_trainers,
            response_mapping_template="$util.toJson($ctx.result)"
        )

        get_all_trainers_resolver.add_depends_on(api_schema)

Get A Single Trainer

Create a text file called get_trainer in the resolver_functions folder and type in the following code.

{
                "version": "2017-02-28",
                "operation": "GetItem",
                "key": {
                "id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
                }
            }

We are getting a single trainer based on their ID.

  with open(os.path.join(dirname, "../resolver_functions/get_trainer"), 'r') as file:
            get_trainer = file.read().replace('\n', '')  

  get_Trainer_resolver = CfnResolver(
            self, 'GetOneQueryResolver',
            api_id=trainers_graphql_api.attr_api_id,
            type_name='Query',
            field_name='getTrainer',
            data_source_name=data_source.name,
            request_mapping_template=get_trainer,
            response_mapping_template="$util.toJson($ctx.result)"
        )

        get_Trainer_resolver.add_depends_on(api_schema)

And that's it.


You can grab the complete code from GitHub

Synthesize a template

AWS CDK apps are effectively only a definition of your infrastructure using code. When CDK apps are executed, they produce (or “synthesize”, in CDK parlance) an AWS CloudFormation template for each stack defined in your application.

To synthesize a CDK app, use the cdk synth command.


Let’s check out the template synthesized from this app.


From the project root directory, activate your virtual environment using

For MacOS/Linux

source .venv/bin/activate

If you are a Windows platform, you would activate the virtualenv like this:

% .venv\Scripts\activate.bat

Synthesize your cdk stack using

cdk synth

Here's my output of the cloud formation template(CdkTrainerStack.template.json), located in the cdk.out folder.

{
  "Resources": {
    "trainersApi": {
      "Type": "AWS::AppSync::GraphQLApi",
      "Properties": {
        "AuthenticationType": "API_KEY",
        "Name": "trainers-api"
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/trainersApi"
      }
    },
    "TrainersApiKey": {
      "Type": "AWS::AppSync::ApiKey",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        }
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/TrainersApiKey"
      }
    },
    "TrainersSchema": {
      "Type": "AWS::AppSync::GraphQLSchema",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "Definition": "type Trainers {    id: ID!    firstName:String!    lastName:String!    age:Int!    specialty:Specialty        }enum Specialty{    BODYBUILDING,    YOUTHFITNESS,    SENIORFITNESS,    CORRECTIVEEXERCISE} type PaginatedTrainers {                    items: [Trainers!]!                    nextToken: String                }                type Query {                    allTrainers(limit: Int, nextToken: String): PaginatedTrainers!                    getTrainer(id: ID!): Trainers                }                type Mutation {                    createTrainer( firstName:String!,    lastName:String!,    age:Int!,    specialty:Specialty): Trainers                    deleteTrainer(id: ID!): Trainers                    updateTrainers(id: ID!,    firstName:String,    lastName:String,    age:Int,    specialty:Specialty):Trainers                }                type Schema {                    query: Query                    mutation: Mutation                }"
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/TrainersSchema"
      }
    },
    "TrainersTableE85CC9B1": {
      "Type": "AWS::DynamoDB::Table",
      "Properties": {
        "KeySchema": [
          {
            "AttributeName": "id",
            "KeyType": "HASH"
          }
        ],
        "AttributeDefinitions": [
          {
            "AttributeName": "id",
            "AttributeType": "S"
          }
        ],
        "BillingMode": "PAY_PER_REQUEST",
        "StreamSpecification": {
          "StreamViewType": "NEW_IMAGE"
        },
        "TableName": "trainers"
      },
      "UpdateReplacePolicy": "Delete",
      "DeletionPolicy": "Delete",
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/TrainersTable/Resource"
      }
    },
    "TrainersDynamoDBRoleE04DDD5F": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": {
                "Service": "appsync.amazonaws.com"
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":iam::aws:policy/AmazonDynamoDBFullAccess"
              ]
            ]
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/TrainersDynamoDBRole/Resource"
      }
    },
    "TrainersDataSource": {
      "Type": "AWS::AppSync::DataSource",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "Name": "TrainersDynamoDataSource",
        "Type": "AMAZON_DYNAMODB",
        "DynamoDBConfig": {
          "AwsRegion": "us-east-2",
          "TableName": {
            "Ref": "TrainersTableE85CC9B1"
          }
        },
        "ServiceRoleArn": {
          "Fn::GetAtt": [
            "TrainersDynamoDBRoleE04DDD5F",
            "Arn"
          ]
        }
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/TrainersDataSource"
      }
    },
    "GetOneQueryResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "FieldName": "getTrainer",
        "TypeName": "Query",
        "DataSourceName": "TrainersDynamoDataSource",
        "RequestMappingTemplate": "{                \"version\": \"2017-02-28\",                \"operation\": \"GetItem\",                \"key\": {                \"id\": $util.dynamodb.toDynamoDBJson($ctx.args.id)                }            }",
        "ResponseMappingTemplate": "$util.toJson($ctx.result)"
      },
      "DependsOn": [
        "TrainersSchema"
      ],
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/GetOneQueryResolver"
      }
    },
    "GetAllQueryResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "FieldName": "allTrainers",
        "TypeName": "Query",
        "DataSourceName": "TrainersDynamoDataSource",
        "RequestMappingTemplate": "{                \"version\": \"2017-02-28\",                \"operation\": \"Scan\",                \"limit\": $util.defaultIfNull($ctx.args.limit, 20),                \"nextToken\": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))            }",
        "ResponseMappingTemplate": "$util.toJson($ctx.result)"
      },
      "DependsOn": [
        "TrainersSchema"
      ],
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/GetAllQueryResolver"
      }
    },
    "CreateTrainerMutationResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "FieldName": "createTrainer",
        "TypeName": "Mutation",
        "DataSourceName": "TrainersDynamoDataSource",
        "RequestMappingTemplate": " {                \"version\": \"2017-02-28\",                \"operation\": \"PutItem\",                \"key\": {                    \"id\": { \"S\": \"$util.autoId()\" }                },                \"attributeValues\": {                    \"firstName\": $util.dynamodb.toDynamoDBJson($ctx.args.firstName),                    \"lastName\": $util.dynamodb.toDynamoDBJson($ctx.args.lastName),                    \"age\": $util.dynamodb.toDynamoDBJson($ctx.args.age),                    \"specialty\": $util.dynamodb.toDynamoDBJson($ctx.args.specialty)                }            }",
        "ResponseMappingTemplate": "$util.toJson($ctx.result)"
      },
      "DependsOn": [
        "TrainersSchema"
      ],
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/CreateTrainerMutationResolver"
      }
    },
    "UpdateMutationResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "FieldName": "updateTrainers",
        "TypeName": "Mutation",
        "DataSourceName": "TrainersDynamoDataSource",
        "RequestMappingTemplate": " {                \"version\": \"2017-02-28\",                \"operation\": \"UpdateItem\",                \"key\":{                    \"id\":$util.dynamodb.toDynamoDBJson($ctx.args.id)                },                \"update\":{                \"expression\": \"SET firstName = :firstName,lastName = :lastName, #ageField =:age,specialty = :specialty\",                \"expressionNames\": {                \"#ageField\": \"age\"                },                \"expressionValues\": {                \":firstName\": $util.dynamodb.toDynamoDBJson($ctx.args.firstName),                \":lastName\": $util.dynamodb.toDynamoDBJson($ctx.args.lastName),                \":age\": $util.dynamodb.toDynamoDBJson($ctx.args.age),                \":specialty\": $util.dynamodb.toDynamoDBJson($ctx.args.specialty)                }                }            }",
        "ResponseMappingTemplate": "$util.toJson($ctx.result)"
      },
      "DependsOn": [
        "TrainersSchema"
      ],
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/UpdateMutationResolver"
      }
    },
    "DeleteMutationResolver": {
      "Type": "AWS::AppSync::Resolver",
      "Properties": {
        "ApiId": {
          "Fn::GetAtt": [
            "trainersApi",
            "ApiId"
          ]
        },
        "FieldName": "deleteTrainer",
        "TypeName": "Mutation",
        "DataSourceName": "TrainersDynamoDataSource",
        "RequestMappingTemplate": "{                \"version\": \"2017-02-28\",                \"operation\": \"DeleteItem\",                \"key\": {                \"id\": $util.dynamodb.toDynamoDBJson($ctx.args.id)                }            }",
        "ResponseMappingTemplate": "$util.toJson($ctx.result)"
      },
      "DependsOn": [
        "TrainersSchema"
      ],
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/DeleteMutationResolver"
      }
    },
    "CDKMetadata": {
      "Type": "AWS::CDK::Metadata",
      "Properties": {
        "Analytics": "v2:deflate64:H4sIAAAAAAAAE0WMyw6CMBBFv4V9HUA2LjWYuNCFgj8wlBoq9JG2aJqm/y4PjaszOffOzSHPCsiSPb7thrZ9GqgyDELtkPakYlaNhjJSKmmdGakj5UP+bCTzU0CtrZcUwhSdDOrudjloPhcnnJknf1/TjgmcxREd1t/tdXF4MRNJ6yUK1TYQ7tgMS7gckXAUECq1upkxRnL1rlMyLWAH2+RpOd+YUTouGFQrPwDtGALgAAAA"
      },
      "Metadata": {
        "aws:cdk:path": "CdkTrainerStack/CDKMetadata/Default"
      }
    }
  }
}

As you can see, this template includes a bunch of resources

  • The trainers API_KEY
  • Schema
  • Data Source
  • All Resolvers

CDK DEPLOY

Now that we've gotten a cloud formation template, it's time to deploy the application.


The first time you deploy an AWS CDK app into an environment (account/region), you’ll need to install a “bootstrap stack”. This stack includes resources that are needed for the toolkit’s operation.


For example, the stack includes an S3 bucket that is used to store templates and assets during the deployment process.

You can use the cdk bootstrap command to install the bootstrap stack into an environment

cdk bootstrap

After successfully installing bootstrap, deploy your app using

cdk deploy

If you deploy is successful, you should get an output similar to this

Screen Shot 2021-05-16 at 08.27.29.png

he CloudFormation Console

CDK apps are deployed through AWS CloudFormation. Each CDK stack maps 1:1 with CloudFormation stack.

This means that you can use the AWS CloudFormation console in order to manage your stacks.

Let’s take a look at the AWS CloudFormation console.

You will likely see something like this (if you don’t, make sure you are in the correct region):

Screen Shot 2021-05-16 at 08.30.55.png

Take note of CdkTrainerStack and CDKToolkit


If you select cdkTrainerStack and open the Resources tab, you will see the physical identities of our resources:

Screen Shot 2021-05-16 at 08.37.04.png

Screen Shot 2021-05-16 at 08.37.13.png

Testing all endpoints

Navigate to AppSync in the AWS console and select the API we just created

Screen Shot 2021-05-16 at 08.41.37.png

Select query on the left side of the screen and add a couple of users to your DB using the CreateTrainer mutation.

Screen Shot 2021-05-16 at 08.43.57.png

Navigate to DynamoDb, click on the trainers table and see all items created.

Screen Shot 2021-05-16 at 08.50.30.png

You can also get all trainers from your DB like so

Screen Shot 2021-05-16 at 08.44.59.png

As an exercise, go ahead and test updateTrainer , deleteTrainer, GetTrainer.

Clean Up Your Stack

To avoid unexpected charges to your account, make sure you clean up your CDK stack.

You can either delete the stack through the AWS CloudFormation console or use

cdk destroy

Conclusion In this post series, we built a GraphQl API using CDK and python, with AWS constructs such as

  • AppSync
  • DynamoDB
  • IAM


Let me know what you think about this piece. I'll also love to know what I should improve on.
Thanks for check this out.
In the next article, we will automate this API by creating a CI/CD pipeline to automatically build and deploy the app when we commit to Github.
So stay tuned.


Till next time my brothers and sisters ✌🏿

No Comments Yet