The Elastic Guru

The Elastic Guru is a community of amazing AWS enthusiasts

We're a place where friendly AWS peeps create, read and share content to ignite curiosity, learning, growth and success in young people, students and others.

Create new account Log in
loading...

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

Ro
I am a software developer living in Cameroon. I build stuff that's easy to use and aesthetically beautiful. I love serverless technologies, SVG and technical writing.
・8 min read

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)


                }
            }
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)
                }
                }


            }
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)
                }
            }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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))
            }
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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)
                }
            }
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

% .venv\Scripts\activate.bat
Enter fullscreen mode Exit fullscreen mode

Synthesize your cdk stack using

cdk synth
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After successfully installing bootstrap, deploy your app using

cdk deploy
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ✌🏿

Discussion (0)

Forem Open with the Forem app