Creating an API with AWS: Part 3: Additional Endpoints

In my post Creating an API with AWS: Part 2: DynamoDB, I updated the simple AWS API endpoint with a DynamoDB database table for data storage. In this post, I’ll add two additional endpoints, with two additional database tables. One of the tables will be a linking table between the two other tables. This allows data stored in one table to be linked to data stored in another. In this example, we’ll add a new endpoint called tag, which we can use to manage tags, and a third endpoint called taglink, which we can use to assign tags to links.

Prerequisites

Before you start, you’ll need the following things:

  1. An AWS account
  2. An Administrator IAM User with which to use the AWS CLI
  3. AWS CLI set up on your local machine
  4. The CloudFormation template from my previous post, Creating an API with AWS: Part 2: DynamoDB.

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/Update DynamoDB tables in the CloudFormation Template

Taking the CloudFormation template (CFT) from my previous post, Creating an API with AWS: Part 2: DynamoDB, update the linkTable resource with a Global Secondary Index. This index will be used to find entries by link value. Add it under the TableName attribute.

GlobalSecondaryIndexes:
    -
      IndexName: "link_index"
      KeySchema:
          -
            AttributeName: "link"
            KeyType: "HASH"
          -
            AttributeName: "link_id"
            KeyType: "RANGE"
       Projection:
           ProjectionType: "KEYS_ONLY"
       ProvisionedThroughput:
           ReadCapacityUnits: "5"
           WriteCapacityUnits: "5"

Next, add two new resources for a DynamoDB tables (tagTable and tagLinkTable) which will be used to store the tags and tag-link mappings. Add them under the linkTable resource:

tagTable:
    Type: AWS::DynamoDB::Table
    Properties:
        AttributeDefinitions:
            -
              AttributeName: "tag_id"
              AttributeType: "S"
            -
              AttributeName: "tag"
              AttributeType: "S"
        KeySchema:
            -
              AttributeName: "tag_id"
              KeyType: "HASH"
            -
              AttributeName: "tag"
              KeyType: "RANGE"
        ProvisionedThroughput:
            ReadCapacityUnits: "5"
            WriteCapacityUnits: "5"
        TableName: "tagTable"
        GlobalSecondaryIndexes:
            -
              IndexName: "tag_index"
              KeySchema:
                -
                  AttributeName: "tag"
                  KeyType: "HASH"
                -
                  AttributeName: "tag_id"
                  KeyType: "RANGE"
              Projection:
                ProjectionType: "KEYS_ONLY"
              ProvisionedThroughput:
                ReadCapacityUnits: "5"
                WriteCapacityUnits: "5"

tagLinkTable:
    Type: AWS::DynamoDB::Table
    Properties:
        AttributeDefinitions:
            -
              AttributeName: "tag_id"
              AttributeType: "S"
            -
              AttributeName: "link_id"
              AttributeType: "S"
        KeySchema:
            -
              AttributeName: "tag_id"
              KeyType: "HASH"
            -
              AttributeName: "link_id"
              KeyType: "RANGE"
         ProvisionedThroughput:
            ReadCapacityUnits: "5"
            WriteCapacityUnits: "5"
        TableName: "tagLinkTable"

Note the the tagTable resource also has a Global Secondary Index to enable filtering by tag name.

Step 2: Update the Lambda Role Permissions in the CloudFormation Template

Next, update the requestHandlerLambdaExecutionRole resource, adding a permission to run database queries to the list of existing database permissions:

PolicyDocument:
    Version: "2012-10-17"
    Statement:
        -
            Effect: "Allow"
            Action:
                - dynamodb:DeleteItem
                - dynamodb:GetItem
                - dynamodb:PutItem
                - dynamodb:Scan
                - dynamodb:UpdateItem
                - dynamodb:Query

This will enable the lambda function to run filtered queries on the database tables.

Step 3: Update the Lambda Function in the CloudFormation Template

Update the requestHandler resource with the new lambda code needed to handle the endpoint requests and database queries:

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 not exist.
DYNAMODB_LINK_TABLE = 'linkTable'
DYNAMODB_TAG_TABLE = 'tagTable'
DYNAMODB_TAG_LINK_TABLE = 'tagLinkTable'

# 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 store_tag(tag):
    tag_id = str(uuid.uuid4())
    dynamodb.put_item(
        TableName=DYNAMODB_TAG_TABLE,
        Item={
            'tag_id': {
                'S': str(tag_id)
            },
            'tag': {
                'S': str(tag)
            }
        }
    )

    return tag_id


def store_taglink(tag_id, link_id):
    dynamodb.put_item(
        TableName=DYNAMODB_TAG_LINK_TABLE,
        Item={
            'tag_id': {
                'S': str(tag_id)
            },
            'link_id': {
                'S': str(link_id)
            }
        }
    )


def get_link_id(link):
    result = dynamodb.query(
        TableName=DYNAMODB_LINK_TABLE,
        IndexName='link_index',
        KeyConditionExpression='link = :link',
        ExpressionAttributeValues={
            ':link': {
                "S": link
            }
        }
    )

    if result['Count'] > 0:
        link_id = result['Items'][0]['link_id']['S']
    else:
        link_id = store_link(link)

    return link_id


def get_tag_id(tag):
    result = dynamodb.query(
        TableName=DYNAMODB_TAG_TABLE,
        IndexName='tag_index',
        KeyConditionExpression='tag = :tag',
        ExpressionAttributeValues={
            ':tag': {
                "S": tag
            }
        }
    )

    if result['Count'] > 0:
        tag_id = result['Items'][0]['tag_id']['S']
    else:
        tag_id = store_tag(tag)

    return tag_id


def get_tags(tag_id=None):
    tags = []

    if tag_id is None:
        result = dynamodb.scan(
            TableName=DYNAMODB_TAG_TABLE,
            Select="SPECIFIC_ATTRIBUTES",
            ProjectionExpression="tag_id,tag"
        )
    else:
        # Run a query with a filter on tag_id on the tagTable
        result = dynamodb.query(
            TableName=DYNAMODB_TAG_TABLE,
            KeyConditionExpression='tag_id = :tag_id',
            ExpressionAttributeValues={
                ':tag_id': {
                    "S": tag_id
                }
            }
        )

    if result['Count'] > 0:
        for x in range(result['Count']):
            tag = {}
            tag['tag_id'] = result['Items'][x]['tag_id']['S']
            tag['tag'] = result['Items'][x]['tag']['S']
            tags.append(tag)

    return tags


def extract_link_result(result):
    links = []
    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 get_links(tag_id=None, link_id=None):
    links = []
    link_id_list = []

    if tag_id is None and link_id is None:
        result = dynamodb.scan(
            TableName=DYNAMODB_LINK_TABLE,
            Select="SPECIFIC_ATTRIBUTES",
            ProjectionExpression="link_id,link"
        )

        links = extract_link_result(result)

    elif link_id is not None:
        # Run a query with a filter on link_id on the linkTable
        result = dynamodb.query(
            TableName=DYNAMODB_LINK_TABLE,
            KeyConditionExpression='link_id = :link_id',
            ExpressionAttributeValues={
                ':link_id': {
                    "S": link_id
                }
            }
        )

        links = extract_link_result(result)
    else:
        # Run a query with a filter on tag_id on the tagLinkTable
        result = dynamodb.query(
            TableName=DYNAMODB_TAG_LINK_TABLE,
            KeyConditionExpression='tag_id = :tag_id',
            ExpressionAttributeValues={
                ':tag_id': {
                    "S": tag_id
                }
            }
        )

        if result['Count'] > 0:
            for x in range(result['Count']):
                link_id_list.append(result['Items'][x]['link_id']['S'])

        # From the retrieved link_ids, retrieve the links from the link table
        if len(link_id_list) > 0:
            for link_id in link_id_list:
                result = dynamodb.query(
                    TableName=DYNAMODB_LINK_TABLE,
                    KeyConditionExpression='link_id = :link_id',
                    ExpressionAttributeValues={
                        ':link_id': {
                            "S": link_id
                        }
                    }
                )
                if result['Count'] > 0:
                    link = {}
                    link['link_id'] = link_id
                    # There should only be one result per link_id
                    link['link'] = result['Items'][0]['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 bad_request_response(error_message, aws_request_id):
    return {
        "statusCode": 400,
        "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['resource'] == '/link':
            if event['httpMethod'] == "POST":
                post_body = json.loads(event['body'])
                link = post_body['link']
                tag = post_body.get('tag')
                tag_id = post_body.get('tag_id')
                if tag_id is None:
                    tag_id = get_tag_id(tag)
                else:
                    tags = get_tags(tag_id)
                    if not tags:
                        return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
                link_id = get_link_id(link)
                store_taglink(tag_id, link_id)
                return_body = json.dumps({"link_id": link_id})
            elif event['httpMethod'] == "GET":
                tag_id = None
                link_id = None
                query_params = event['queryStringParameters']
                if query_params is not None:
                    tag_id = query_params.get('tag_id', None)
                    tag = query_params.get('tag', None)
                    link_id = query_params.get('link_id', None)
                    if tag_id is None and tag is not None:
                        tag_id = get_tag_id(tag)
                links = get_links(tag_id, link_id)
                return_body = json.dumps(links)
        elif event['resource'] == '/tag':
            if event['httpMethod'] == "POST":
                post_body = json.loads(event['body'])
                tag = post_body.get('tag')
                tag_id = get_tag_id(tag)
                return_body = json.dumps({"tag_id": tag_id})
            elif event['httpMethod'] == "GET":
                tag_id = None
                query_params = event['queryStringParameters']
                if query_params is not None:
                    tag_id = query_params.get('tag_id', None)
                    tag = query_params.get('tag', None)
                    if tag_id is None and tag is not None:
                        tag_id = get_tag_id(tag)
                tags = get_tags(tag_id)
                return_body = json.dumps(tags)
        elif event['resource'] == '/taglink':
            if event['httpMethod'] == "POST":
                post_body = json.loads(event['body'])
                tag_id = post_body.get('tag_id')
                link_id = post_body.get('link_id')
                tags = get_tags(tag_id)
                links = get_links(tag_id=None, link_id=link_id)
                if not tags:
                    return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
                if not links:
                    return bad_request_response('Unknown link_id ' + link_id, context.aws_request_id)
                store_taglink(tag_id, link_id)
                return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
        # 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.
        return error_response(str(err), context.aws_request_id)

Step 4: Add Two New API Resources in the CloudFormation Template

Add two API Gateway resources, one for the tag endpoint, and one for the taglink endpoint.

tag:
    Type: AWS::ApiGateway::Resource
    Properties:
        RestApiId: !Ref ApplicationAPI
        ParentId: !GetAtt [ApplicationAPI, RootResourceId]
        PathPart: 'tag'
    DependsOn:
        - ApplicationAPI

taglink:
    Type: AWS::ApiGateway::Resource
    Properties:
        RestApiId: !Ref ApplicationAPI
        ParentId: !GetAtt [ApplicationAPI, RootResourceId]
        PathPart: 'taglink'
    DependsOn:
        - ApplicationAPI

Step 5: Add Methods for the New Endpoints in the CloudFormation Template

Add methods for the tag and taglink endpoints. Update the Deployment resource to depend on the OptionsMethodTagLink resource, which should be the last endpoint method to be created, with the rest in a chain.

    
ProxyResourceTagGET:
    Type: AWS::ApiGateway::Method
    Properties:
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref tag
        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:
        - ProxyResourceLinkPOST

ProxyResourceTagPOST:
    Type: AWS::ApiGateway::Method
    Properties:
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref tag
        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:
        - ProxyResourceTagGET

OptionsMethodTag:
    Type: AWS::ApiGateway::Method
    Properties:
        AuthorizationType: NONE
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref tag
        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:
        - ProxyResourceTagPOST

ProxyResourceTagLinkGET:
    Type: AWS::ApiGateway::Method
    Properties:
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref taglink
        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:
        - ProxyResourceTagPOST

ProxyResourceTagLinkPOST:
    Type: AWS::ApiGateway::Method
    Properties:
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref taglink
        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:
        - ProxyResourceTagLinkGET

OptionsMethodTagLink:
    Type: AWS::ApiGateway::Method
    Properties:
        AuthorizationType: NONE
        RestApiId: !Ref ApplicationAPI
        ResourceId: !Ref taglink
        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:
        - ProxyResourceTagLinkPOST

Deployment: 
    DependsOn: "OptionsMethodTagLink"
    Type: AWS::ApiGateway::Deployment
    Properties: 
      RestApiId: 
        Ref: "ApplicationAPI"
      Description: "Application API deployment"
      StageName: "prod"

Step 6: Update the Outputs in the CloudFormation Template

Update the Outputs to include each endpoint URL, as well as the base URL for the API.

Outputs:
  APIBaseUrl:
    Description: Application API Gateway base URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod'

  APILinkUrl:
    Description: Application API Gateway link endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/link'

  APITagUrl:
    Description: Application API Gateway tag endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/tag'

  APITagLinkUrl:
    Description: Application API Gateway taglink endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/taglink'

Step 7: Launch the CloudFormation Template

After an update to the Description, the CloudFormation template should now look something like the below:

AWSTemplateFormatVersion: "2010-09-09"
Description: Creates a simple API with three endpoints that can be called to POST/GET links and tags and tag links, with a DynamoDB database
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"
          GlobalSecondaryIndexes:
            -
              IndexName: "link_index"
              KeySchema:
                -
                  AttributeName: "link"
                  KeyType: "HASH"
                -
                  AttributeName: "link_id"
                  KeyType: "RANGE"
              Projection:
                ProjectionType: "KEYS_ONLY"
              ProvisionedThroughput:
                ReadCapacityUnits: "5"
                WriteCapacityUnits: "5"
          
    tagTable:
        Type: AWS::DynamoDB::Table
        Properties:
          AttributeDefinitions:
            -
              AttributeName: "tag_id"
              AttributeType: "S"
            -
              AttributeName: "tag"
              AttributeType: "S"
          KeySchema:
            -
              AttributeName: "tag_id"
              KeyType: "HASH"
            -
              AttributeName: "tag"
              KeyType: "RANGE"
          ProvisionedThroughput:
            ReadCapacityUnits: "5"
            WriteCapacityUnits: "5"
          TableName: "tagTable"
          GlobalSecondaryIndexes:
            -
              IndexName: "tag_index"
              KeySchema:
                -
                  AttributeName: "tag"
                  KeyType: "HASH"
                -
                  AttributeName: "tag_id"
                  KeyType: "RANGE"
              Projection:
                ProjectionType: "KEYS_ONLY"
              ProvisionedThroughput:
                ReadCapacityUnits: "5"
                WriteCapacityUnits: "5"

          
    tagLinkTable:
        Type: AWS::DynamoDB::Table
        Properties:
          AttributeDefinitions:
            -
              AttributeName: "tag_id"
              AttributeType: "S"
            -
              AttributeName: "link_id"
              AttributeType: "S"
          KeySchema:
            -
              AttributeName: "tag_id"
              KeyType: "HASH"
            -
              AttributeName: "link_id"
              KeyType: "RANGE"
          ProvisionedThroughput:
            ReadCapacityUnits: "5"
            WriteCapacityUnits: "5"
          TableName: "tagLinkTable"
                
    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
                                - dynamodb:Query
                            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 not exist.
                DYNAMODB_LINK_TABLE = 'linkTable'
                DYNAMODB_TAG_TABLE = 'tagTable'
                DYNAMODB_TAG_LINK_TABLE = 'tagLinkTable'

                # 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 store_tag(tag):
                    tag_id = str(uuid.uuid4())
                    dynamodb.put_item(
                        TableName=DYNAMODB_TAG_TABLE,
                        Item={
                            'tag_id': {
                                'S': str(tag_id)
                            },
                            'tag': {
                                'S': str(tag)
                            }
                        }
                    )

                    return tag_id


                def store_taglink(tag_id, link_id):
                    dynamodb.put_item(
                        TableName=DYNAMODB_TAG_LINK_TABLE,
                        Item={
                            'tag_id': {
                                'S': str(tag_id)
                            },
                            'link_id': {
                                'S': str(link_id)
                            }
                        }
                    )


                def get_link_id(link):
                    result = dynamodb.query(
                        TableName=DYNAMODB_LINK_TABLE,
                        IndexName='link_index',
                        KeyConditionExpression='link = :link',
                        ExpressionAttributeValues={
                            ':link': {
                                "S": link
                            }
                        }
                    )

                    if result['Count'] > 0:
                        link_id = result['Items'][0]['tag_id']['S']
                    else:
                        link_id = store_link(link)

                    return link_id


                def get_tag_id(tag):
                    result = dynamodb.query(
                        TableName=DYNAMODB_TAG_TABLE,
                        IndexName='tag_index',
                        KeyConditionExpression='tag = :tag',
                        ExpressionAttributeValues={
                            ':tag': {
                                "S": tag
                            }
                        }
                    )

                    if result['Count'] > 0:
                        tag_id = result['Items'][0]['tag_id']['S']
                    else:
                        tag_id = store_tag(tag)

                    return tag_id


                def get_tags(tag_id=None):
                    tags = []

                    if tag_id is None:
                        result = dynamodb.scan(
                            TableName=DYNAMODB_TAG_TABLE,
                            Select="SPECIFIC_ATTRIBUTES",
                            ProjectionExpression="tag_id,tag"
                        )
                    else:
                        # Run a query with a filter on tag_id on the tagTable
                        result = dynamodb.query(
                            TableName=DYNAMODB_TAG_TABLE,
                            KeyConditionExpression='tag_id = :tag_id',
                            ExpressionAttributeValues={
                                ':tag_id': {
                                    "S": tag_id
                                }
                            }
                        )

                    if result['Count'] > 0:
                        for x in range(result['Count']):
                            tag = {}
                            tag['tag_id'] = result['Items'][x]['tag_id']['S']
                            tag['tag'] = result['Items'][x]['tag']['S']
                            tags.append(tag)

                    return tags


                def extract_link_result(result):
                    links = []
                    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 get_links(tag_id=None, link_id=None):
                    links = []
                    link_id_list = []

                    if tag_id is None and link_id is None:
                        result = dynamodb.scan(
                            TableName=DYNAMODB_LINK_TABLE,
                            Select="SPECIFIC_ATTRIBUTES",
                            ProjectionExpression="link_id,link"
                        )

                        links = extract_link_result(result)

                    elif link_id is not None:
                        # Run a query with a filter on link_id on the linkTable
                        result = dynamodb.query(
                            TableName=DYNAMODB_LINK_TABLE,
                            KeyConditionExpression='link_id = :link_id',
                            ExpressionAttributeValues={
                                ':link_id': {
                                    "S": link_id
                                }
                            }
                        )

                        links = extract_link_result(result)
                    else:
                        # Run a query with a filter on tag_id on the tagLinkTable
                        result = dynamodb.query(
                            TableName=DYNAMODB_TAG_LINK_TABLE,
                            KeyConditionExpression='tag_id = :tag_id',
                            ExpressionAttributeValues={
                                ':tag_id': {
                                    "S": tag_id
                                }
                            }
                        )

                        if result['Count'] > 0:
                            for x in range(result['Count']):
                                link_id_list.append(result['Items'][x]['link_id']['S'])

                        # From the retrieved link_ids, retrieve the links from the link table
                        if len(link_id_list) > 0:
                            for link_id in link_id_list:
                                result = dynamodb.query(
                                    TableName=DYNAMODB_LINK_TABLE,
                                    KeyConditionExpression='link_id = :link_id',
                                    ExpressionAttributeValues={
                                        ':link_id': {
                                            "S": link_id
                                        }
                                    }
                                )
                                if result['Count'] > 0:
                                    link = {}
                                    link['link_id'] = link_id
                                    # There should only be one result per link_id
                                    link['link'] = result['Items'][0]['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 bad_request_response(error_message, aws_request_id):
                    return {
                        "statusCode": 400,
                        "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['resource'] == '/link':
                            if event['httpMethod'] == "POST":
                                post_body = json.loads(event['body'])
                                link = post_body['link']
                                tag = post_body.get('tag')
                                tag_id = post_body.get('tag_id')
                                if tag_id is None:
                                    tag_id = get_tag_id(tag)
                                else:
                                    tags = get_tags(tag_id)
                                    if not tags:
                                        return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
                                link_id = get_link_id(link)
                                store_taglink(tag_id, link_id)
                                return_body = json.dumps({"link_id": link_id})
                            elif event['httpMethod'] == "GET":
                                tag_id = None
                                link_id = None
                                query_params = event['queryStringParameters']
                                if query_params is not None:
                                    # query_params = json.loads(query_params)
                                    tag_id = query_params.get('tag_id', None)
                                    tag = query_params.get('tag', None)
                                    link_id = query_params.get('link_id', None)
                                    if tag_id is None and tag is not None:
                                        tag_id = get_tag_id(tag)
                                links = get_links(tag_id, link_id)
                                return_body = json.dumps(links)
                        elif event['resource'] == '/tag':
                            if event['httpMethod'] == "POST":
                                post_body = json.loads(event['body'])
                                tag = post_body.get('tag')
                                tag_id = store_tag(tag)
                                return_body = json.dumps({"tag_id": tag_id})
                            elif event['httpMethod'] == "GET":
                                tag_id = None
                                query_params = event['queryStringParameters']
                                if query_params is not None:
                                    tag_id = query_params.get('tag_id', None)
                                    tag = query_params.get('tag', None)
                                    if tag_id is None and tag is not None:
                                        tag_id = get_tag_id(tag)
                                tags = get_tags(tag_id)
                                return_body = json.dumps(tags)
                        elif event['resource'] == '/taglink':
                            if event['httpMethod'] == "POST":
                                post_body = json.loads(event['body'])
                                tag_id = post_body.get('tag_id')
                                link_id = post_body.get('link_id')
                                tags = get_tags(tag_id)
                                links = get_links(tag_id=None, link_id=link_id)
                                if not tags:
                                    return bad_request_response('Unknown tag_id ' + tag_id, context.aws_request_id)
                                if not links:
                                    return bad_request_response('Unknown link_id ' + link_id, context.aws_request_id)
                                store_taglink(tag_id, link_id)
                                return_body = json.dumps({"tag_id": tag_id, "link_id": link_id})
                        # 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.
                        return 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

    tag:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApplicationAPI
            ParentId: !GetAtt [ApplicationAPI, RootResourceId]
            PathPart: 'tag'
        DependsOn:
            - ApplicationAPI

    taglink:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApplicationAPI
            ParentId: !GetAtt [ApplicationAPI, RootResourceId]
            PathPart: 'taglink'
        DependsOn:
            - ApplicationAPI
    
    ProxyResourceLinkGET:
        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
            
    ProxyResourceLinkPOST:
        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: 
            - ProxyResourceLinkGET
              
    OptionsMethodLink:
        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: 
            - ProxyResourceLinkPOST

    ProxyResourceTagGET:
        Type: AWS::ApiGateway::Method
        Properties:
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref tag
            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:
            - ProxyResourceLinkPOST

    ProxyResourceTagPOST:
        Type: AWS::ApiGateway::Method
        Properties:
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref tag
            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:
            - ProxyResourceTagGET

    OptionsMethodTag:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref tag
            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:
            - ProxyResourceTagPOST

    ProxyResourceTagLinkGET:
        Type: AWS::ApiGateway::Method
        Properties:
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref taglink
            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:
            - ProxyResourceTagPOST

    ProxyResourceTagLinkPOST:
        Type: AWS::ApiGateway::Method
        Properties:
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref taglink
            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:
            - ProxyResourceTagLinkGET

    OptionsMethodTagLink:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApplicationAPI
            ResourceId: !Ref taglink
            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:
            - ProxyResourceTagLinkPOST
                
    Deployment: 
      DependsOn: "OptionsMethodTagLink"
      Type: AWS::ApiGateway::Deployment
      Properties: 
        RestApiId: 
          Ref: "ApplicationAPI"
        Description: "Application API deployment"
        StageName: "prod"
Outputs:
  APIBaseUrl:
    Description: Application API Gateway base URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod'

  APILinkUrl:
    Description: Application API Gateway link endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/link'

  APITagUrl:
    Description: Application API Gateway tag endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/tag'

  APITagLinkUrl:
    Description: Application API Gateway taglink endpoint URL
    Value: !Join
        - ''
        - - 'https://'
          - !Ref ApplicationAPI
          - '.execute-api.'
          - !Ref AWS::Region
          - '.amazonaws.com/prod/taglink'

Save the CFT to a file called setupapidb2.yml and launch it with the following command:

$ aws cloudformation create-stack --stack-name apidb2 --template-body file://setupapidb2.yml --capabilities CAPABILITY_NAMED_IAM

Poll the progress of the CloudFormation stack with the following command:

$ aws cloudformation describe-stacks --stack-name apidb2

Once complete, the value of “StackStatus” in the response should be “CREATE_COMPLETE” and you should have four API URLs in the “Outputs” section, under each “OutputValue”. The links should be of the form: “https://xxxxxxxxxx.execute-api.your-region.amazonaws.com/prod/ENDPOINT” for each of the endpoints link, tag and taglink. There should also be a base URL under “APIBaseUrl”. This is the URL of the API, minus any specific endpoint.

Step 8: Test the API

Test that everything is working by using the bash test script below. You may need to install curl and jq first with sudo yum install epel-release -y; sudo yum install curl jq or similar, depending on your operating system. The jq package is useful for parsing and displaying json. Once both are installed, copy the script below to a file called test.sh. Be sure to give it executable permissions (sudo chmod u+x test.sh for linux). Then run it with the API Base URL as the sole argument, e.g:

$ /usr/bin/bash test.sh https://xxxxxxxxxx.execute-api.your-region.amazonaws.com/prod

The test script is below:

BASEURL=$1

echo "=========================="
echo "Test POST /link link=https://www.allthecoding.com, tag=blog"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.allthecoding.com\", \"tag\": \"blog\"}" $BASEURL/link)
echo $resp
link_id1=$(echo $resp | jq '.link_id' | tr -d '"')
echo $link_id1

echo "=========================="
echo "Test GET /tag tag=blog"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag?tag=blog)
echo $resp
tag_id1=$(echo $resp | jq '.[0].tag_id' | tr -d '"')
echo $tag_id1

echo "=========================="
echo "Test POST /tag tag=stuff"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"tag\": \"stuff\"}" $BASEURL/tag)
echo $resp
tag_id2=$(echo $resp | jq '.tag_id' | tr -d '"')
echo $tag_id2

echo "=========================="
echo "Test POST /link link=https://www.google.com, tag=stuff"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.google.com\", \"tag\": \"stuff\"}" $BASEURL/link)
echo $resp
link_id2=$(echo $resp | jq '.link_id' | tr -d '"')
echo $link_id2

echo "=========================="
echo "Test POST /link link=https://www.google.com, tag_id=$tag_id1"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.google.com\", \"tag_id\": \"$tag_id1\"}" $BASEURL/link)
echo $resp
link_id2=$(echo $resp | jq '.link_id' | tr -d '"')
echo $link_id2

echo "=========================="
echo "Test POST /link link=https://www.google.com, tag_id=invalid. Expect 400 response: Unknown tag_id invalid"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.google.com\", \"tag_id\": \"invalid\"}" $BASEURL/link)
echo $resp
link_id2=$(echo $resp | jq '.link_id' | tr -d '"')
echo $link_id2

echo "=========================="
echo "Test GET /link"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link)
echo $resp

echo "=========================="
echo "Test GET /link tag_id=$tag_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link?tag_id=$tag_id1)
echo $resp

echo "=========================="
echo "Test GET /link tag=blog"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link?tag=blog)
echo $resp

echo "=========================="
echo "Test GET /link link_id=$link_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link?link_id=$link_id1)
echo $resp

echo "=========================="
echo "Test GET /tag tag_id=$tag_id1"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag?tag_id=$tag_id1)
echo $resp

echo "=========================="
echo "Test GET /tag"
resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag)
echo $resp

echo "=========================="
echo "Test POST /taglink tag_id=$tag_id1, link_id=$link_id1"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink)
echo $resp

echo "=========================="
echo "Test POST /taglink tag_id=invalid, link_id=$link_id1. Expect 400 response: Unknown tag_id invalid"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"invalid\"}" $BASEURL/taglink)
echo $resp

echo "=========================="
echo "Test POST /taglink tag_id=$tag_id1, link_id=invalid. Expect 400 response: Unknown link_id invalid"
resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"invalid\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink)
echo $resp

If all goes well, the script should run with no errors apart from the three expected error responses, where the script will print the expected error response before making the request.

Step 9: 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 apidb2

You will then need to remove any logs that were generated by the Lambda function. To do this:

  1. Log into the AWS Console (your AWS account) using your Administrator IAM user, and navigate to CloudWatch.
  2. On the left-hand side, select Logs -> Log Groups.
  3. Select ‘aws/lambda/requestHandler’.
  4. 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 has three endpoints and three DynamoDB database tables. You’ve also learned how to add Global Secondary Indexes for filter queries. If you want to find out how to add AWS Cognito authentication to the API, read my follow-on post, Creating an API with AWS: Part 4: Cognito Authentication.

Thanks for reading!