In my post Creating an API with AWS, I demonstrated how to use a CloudFormation template to create a simple API with AWS. In this post, I’m going to walk through adding a DynamoDB database and POST method, and updating GET requests to pull data from the database.
Prerequisites
Before you start, you’ll need the following things:
- An AWS account
- An Administrator IAM User with which to use the AWS CLI
- AWS CLI set up on your local machine
- The CloudFormation template from my previous post, Creating an API with AWS
Note that AWS will charge you for creating and hosting this API if you don’t have Free Tier on your account.
Step 1: Add a DynamoDB Table to the CloudFormation Template
AWS DynamoDB is a fast, NoSQL database that is built around key-value pairs. For a simple web application, it is a good choice for data storage.
Taking the CloudFormation template (CFT) from my previous post, Creating an API with AWS, we can add a resource for a DynamoDB table which we’ll use to store the links under the Resources: line:
linkTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "link_id"
AttributeType: "S"
-
AttributeName: "link"
AttributeType: "S"
KeySchema:
-
AttributeName: "link_id"
KeyType: "HASH"
-
AttributeName: "link"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "linkTable"
This table will be called linkTable and have two attributes (fields): link_id, which will contain a unique id; and link, which will store the URL value. In the KeySchema section, we’ll define the link_id as the primary key and the link attribute as the sort key.
Step 2: Update the Lambda Execution Role in the CloudFormation Template
Next, we need to update the requestHandlerLambdaExecutionRole to enable the Lambda function to perform database operations. To do this, add the following policy underneath the existing policy named root:
-
PolicyName: "db_access"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- dynamodb:DeleteItem
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:UpdateItem
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*"
This policy will allow the Lambda function to delete, get, add, and update database table items as well as perform full table scans.
Step 3: Update the Lambda Function to use the DynamoDB Database
Replace the requestHandler section of the CFT with the following Lambda function, which will check if a request is a GET or a POST, and store or retrieve data to/from the database as required:
requestHandler:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: !Sub |
import json
import boto3
import uuid
# Establish credentials
session_var = boto3.session.Session()
credentials = session_var.get_credentials()
""" --- Parameters --- """
# DynamoDB tables, read lambda function environment variables or initialize with defaults if they do not exist.
DYNAMODB_LINK_TABLE = 'linkTable'
# Initialize DynamoDB Client
dynamodb = boto3.client('dynamodb')
def store_link(link):
link_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_LINK_TABLE,
Item={
'link_id': {
'S': str(link_id)
},
'link': {
'S': str(link)
}
}
)
return link_id
def get_links():
links = []
result = dynamodb.scan(
TableName=DYNAMODB_LINK_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="link_id,link"
)
if result['Count'] > 0:
for x in range(result['Count']):
link = {}
link['link_id'] = result['Items'][x]['link_id']['S']
link['link'] = result['Items'][x]['link']['S']
links.append(link)
return links
def error_response(error_message, aws_request_id):
return {
"statusCode": 500,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def lambda_handler(event, context):
try:
print(event)
print(context)
return_body = ""
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
link = post_body['link']
link_id = store_link(link)
return_body = json.dumps({"link_id": link_id})
elif event['httpMethod'] == "GET":
links = get_links()
return_body = json.dumps(links)
# Because this Lambda function is called by an API Gateway proxy integration
# the result object must use the following structure.
return {
"statusCode": 200,
"isBase64Encoded": False,
"body": return_body,
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
except Exception as err:
print(str(err))
# If there is an error during processing, catch it and return
# from the Lambda function successfully. Specify a 500 HTTP status
# code and provide an error message in the body. This will provide a
# more meaningful error response to the end client.
error_response(str(err), context.aws_request_id)
Description: Lambda function for processing api endpoint requests
FunctionName: requestHandler
Handler: index.lambda_handler
Role:
Fn::GetAtt:
- "requestHandlerLambdaExecutionRole"
- "Arn"
Runtime: python3.8
DependsOn:
- requestHandlerLambdaExecutionRole
Step 4: Add a POST Method to the API in the CloudFormation Template
Add a POST method to the API so that new links can be added. To do this, add another AWS::APIGateway::Method resource under ProxyResourceGET called ProxyResourcePOST:
ProxyResourcePOST:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: POST
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ApplicationAPI
Step 5: Launch the CloudFormation Template
Update the Description field to something that better describes what this CFT is building, e.g. “Creates a simple API endpoint that can be called to POST a link and store it in a database, or GET a list of links”. Save the CFT to a file called setupapidb.yml. The complete CFT should now look as below:
AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a simple API endpoint that can be called to POST a link and store it in a database, or GET a list of links
Resources:
linkTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
-
AttributeName: "link_id"
AttributeType: "S"
-
AttributeName: "link"
AttributeType: "S"
KeySchema:
-
AttributeName: "link_id"
KeyType: "HASH"
-
AttributeName: "link"
KeyType: "RANGE"
ProvisionedThroughput:
ReadCapacityUnits: "5"
WriteCapacityUnits: "5"
TableName: "linkTable"
requestHandlerLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: Allow
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
-
PolicyName: "root"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- logs:CreateLogGroup
- logs:PutLogEvents
- logs:CreateLogStream
Resource: "*"
-
PolicyName: "db_access"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- dynamodb:DeleteItem
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:UpdateItem
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/*"
RoleName: requestHandlerLambdaExecutionRole
requestHandler:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: !Sub |
import json
import boto3
import uuid
# Establish credentials
session_var = boto3.session.Session()
credentials = session_var.get_credentials()
""" --- Parameters --- """
# DynamoDB tables, read lambda function environment variables or initialize with defaults if they do not exist.
DYNAMODB_LINK_TABLE = 'linkTable'
# Initialize DynamoDB Client
dynamodb = boto3.client('dynamodb')
def store_link(link):
link_id = str(uuid.uuid4())
dynamodb.put_item(
TableName=DYNAMODB_LINK_TABLE,
Item={
'link_id': {
'S': str(link_id)
},
'link': {
'S': str(link)
}
}
)
return link_id
def get_links():
links = []
result = dynamodb.scan(
TableName=DYNAMODB_LINK_TABLE,
Select="SPECIFIC_ATTRIBUTES",
ProjectionExpression="link_id,link"
)
if result['Count'] > 0:
for x in range(result['Count']):
link = {}
link['link_id'] = result['Items'][x]['link_id']['S']
link['link'] = result['Items'][x]['link']['S']
links.append(link)
return links
def error_response(error_message, aws_request_id):
return {
"statusCode": 500,
"isBase64Encoded": False,
"body": json.dumps({
"Error": error_message,
"Reference": aws_request_id,
}),
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
def lambda_handler(event, context):
try:
print(event)
print(context)
return_body = ""
if event['httpMethod'] == "POST":
post_body = json.loads(event['body'])
link = post_body['link']
link_id = store_link(link)
return_body = json.dumps({"link_id": link_id})
elif event['httpMethod'] == "GET":
links = get_links()
return_body = json.dumps(links)
# Because this Lambda function is called by an API Gateway proxy integration
# the result object must use the following structure.
return {
"statusCode": 200,
"isBase64Encoded": False,
"body": return_body,
"headers": {
'Access-Control-Allow-Origin': '*',
},
}
except Exception as err:
print(str(err))
# If there is an error during processing, catch it and return
# from the Lambda function successfully. Specify a 500 HTTP status
# code and provide an error message in the body. This will provide a
# more meaningful error response to the end client.
error_response(str(err), context.aws_request_id)
Description: Lambda function for processing api endpoint requests
FunctionName: requestHandler
Handler: index.lambda_handler
Role:
Fn::GetAtt:
- "requestHandlerLambdaExecutionRole"
- "Arn"
Runtime: python3.8
DependsOn:
- requestHandlerLambdaExecutionRole
ApplicationAPI:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ApplicationAPI
Description: Simple API for an application
DependsOn:
- requestHandler
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt
- requestHandler
- Arn
Action: 'lambda:InvokeFunction'
Principal: apigateway.amazonaws.com
SourceArn: !Join ["", [!Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:", !Ref ApplicationAPI, "/*"]]
DependsOn:
- ApplicationAPI
link:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref ApplicationAPI
ParentId: !GetAtt [ApplicationAPI, RootResourceId]
PathPart: 'link'
DependsOn:
- ApplicationAPI
ProxyResourceGET:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ApplicationAPI
ProxyResourcePOST:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: POST
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations
IntegrationResponses:
-
StatusCode: 200
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
DependsOn:
- ApplicationAPI
OptionsMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
RestApiId: !Ref ApplicationAPI
ResourceId: !Ref link
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
-
StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: NEVER
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
-
StatusCode: 200
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
DependsOn:
- ProxyResourceGET
Deployment:
DependsOn: "OptionsMethod"
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: "ApplicationAPI"
Description: "Application API deployment"
StageName: "prod"
Outputs:
APIUrl:
Description: Application API Gateway link endpoint URL
Value: !Join
- ''
- - 'https://'
- !Ref ApplicationAPI
- '.execute-api.'
- !Ref AWS::Region
- '.amazonaws.com/prod/link'
Launch the CFT with the following command:
$ aws cloudformation create-stack --stack-name apidb --template-body file://setupapidb.yml --capabilities CAPABILITY_NAMED_IAM
Poll the progress of the CloudFormation stack with the following command:
$ aws cloudformation describe-stacks --stack-name apidb
Once complete, the value of “StackStatus” in the response should be “CREATE_COMPLETE” and you should have the API endpoint link in the “Outputs” section, under “OutputValue”. The link should be of the form: “https://xxxxxxxxxx.execute-api.your-region.amazonaws.com/prod/link”.
Step 6: Test the API
Test that everything is working by using curl. You may need to install curl first with sudo yum install curl or similar, depending on your operating system. Once installed, issue a curl POST request like the following. Be sure to replace the endpoint url with your own:
$ curl -v -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.google.com\"}" https://xxxxxxxxxx.execute-api.your-region.amazonaws.com/prod/link
If the request is successful, you should see a response with a lot of connection information, and at the end of it, a line similar to this one:
{"link_id": "177173df-0e92-41a9-b2df-e8722443fd8a"}
That is the link_id of the link database entry just created.
Now try a GET request with the following curl request:
$ curl https://xxxxxxxxxx.execute-api.your-region.amazonaws.com/prod/link
If all is working, you should get a response like the below:
$ [{"link_id": "177173df-0e92-41a9-b2df-e8722443fd8a", "link": "https://www.google.com"}]
Try POSTing different links and then calling GET again. You should see the list of links getting larger.
Step 7: Clean Up
If you don’t want to keep your API (note that AWS will charge you for it if you don’t have Free Tier on your account), delete the CFT stack (all resources created) by running:
$ aws cloudformation delete-stack --stack-name apidb
You will then need to remove any logs that were generated by the Lambda function. To do this:
- Log into the AWS Console (your AWS account) using your Administrator IAM user, and navigate to CloudWatch.
- On the left-hand side, select Logs -> Log Groups.
- Select ‘aws/lambda/requestHandler’.
- Under Actions, select ‘Delete log groups’ and confirm.
Conclusion
You should now be able to create a simple API on AWS with a CloudFormation template, that stores and retrieves data to and from a DynamoDB database. If you’re interested in adding additional endpoints and databases, see my follow-on post, Creating an API with AWS: Part 3: Additional Endpoints.
Thanks for reading!

2 Responses
[…] Creating an API with AWS: Part 2: DynamoDB November 20, 2021 […]
[…] Creating an API with AWS: Part 2: DynamoDB November 20, 2021 […]