Create a serverless RESTful API with API Gateway, CloudFormation, Lambda, and DynamoDB

Michael Wittig – 26 Jul 2016

This article teaches you how to create a serverless RESTful API on AWS. You will use CloudFormation to define the API Gateway in combination with Lambda to implement the functionality. DynamoDB is used to store the data. The example’s source code is available on GitHub and can be used to speed up your project.

If you are interested in defining the API in OpenAPI Specification or Swagger Specification read Create a serverless RESTful API with API Gateway, Swagger, Lambda, and DynamoDB instead!
If you are interested in deploying the API with the Serverless Framework read Create a serverless RESTful API with the Serverless Framework powered by API Gateway, Lambda, and DynamoDB instead!

Implementing a RESTful API with API Gateway, Lambda, and DynamoDB

API Gateway provides an HTTP API endpoint that is fully configurable. You define the HTTP resources (like /user), the HTTP methods on that resources (like POST, GET, DELETE, …) and the integration (e.g. Lambda function) that should be called to process the request. The Lambda function can then run whatever logic is needed to answer the request. API Gateway responds to the caller with the result of the Lambda function. The following figure demonstrates this flow.

API Gateway flow from client request to Lambda and back

If we zoom into the API Gateway component of the previous figure, we see what happens inside the API Gateway.

API Gateway internals

If you want to define a REST API you need to specify:

  • Resources (e.g. GET /user)
  • Methods on each resource (e.g. GET /user)
  • Input
  • Body Model
  • Headers
  • Path parameters (e.g. GET /user/:userId)
  • Query parameters (e.g. GET /user?limit=10)
  • Mapping HTTP input to integration input
  • Integrations (e.g. Lambda functions)
  • Mapping integration output to HTTP output
  • Output
  • Body Model
  • Headers

You can use CloudFormation to define a REST API.

Defining a RESTful API with CloudFormation

CloudFormation uses the JavaScript Object Notation (JSON) to describe Resources like REST APIs. The JSON file is called a template and is the blueprint of your AWS infrastructure. A REST API in API Gateway composed of three components:

  • Models: Define the input/output of the data
  • Resources and Methods: e.g. GET /user to retrieve a list of all users
  • Endpoint: e.g. https://api.somename.com/v1

You will now learn how to describe those components in CloudFormation. Remember that a CloudFormation template has the following structure:

{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Lambda and API Gateway",
"Resources": {
[...]
}
}

Everything that you will learn now happens inside the Resources block. The first resource is a REST API container to group all the following stuff.

"RestApi": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "API Gateway ToDo"
}
}

Describing Models

Models are defined with JSON Schema. The ToDo applications stores users with three required fields:

  • uid: User id
  • email
  • phone

Mapped to a model in CloudFormation, this looks like that:

"UserModel": {
"Type": "AWS::ApiGateway::Model",
"Properties": {
"ContentType": "application/json",
"Name": "User",
"RestApiId": {"Ref": "RestApi"},
"Schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "User",
"type": "object",
"additionalProperties": false,
"properties": {
"uid": {
"type": "string"
},
"email": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"required": ["uid", "email", "phone"]
}
}
}

You can also reference a model in a model for example if you want to define a list of users:

"UsersModel": {
"Type": "AWS::ApiGateway::Model",
"Properties": {
"ContentType": "application/json",
"Name": "Users",
"RestApiId": {"Ref": "RestApi"},
"Schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Users",
"type": "array",
"items": {
"$ref": {"Fn::Join": ["", ["https://apigateway.amazonaws.com/restapis/", {"Ref": "RestApi"}, "/models/", {"Ref": "UserModel"}]]}
}
}
}
}

Now we need to define HTTP resources and methods.

Describing Resources and Methods

We want our API to expose the list of all users under GET /user. In CloudFormation we need to define the HTTP resource /user and the HTTP Method GET. We also define two optional query parameters limit and next to provide a paging interface.

"UsersResource": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"RestApiId": {"Ref": "RestApi"},
"ParentId": {"Fn::GetAtt": ["RestApi", "RootResourceId"]},
"PathPart": "user"
}
},
"UsersGet": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
[...]
"Integration": {
"Type": "AWS",
"IntegrationHttpMethod": "POST",
"Uri": {"Fn::Join" : ["", ["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["Lambda", "Arn"]}, "/invocations"]]},
"IntegrationResponses": [{
[...]
},
"RequestParameters": {
"method.request.querystring.limit": false,
"method.request.querystring.next": false
},
"MethodResponses": [{
"ResponseModels": {
"application/json": {"Ref": "UsersModel"}
},
"ResponseParameters": {
"method.response.header.Link": true
},
"StatusCode": 200
}]
}
}

I left the Integration block empty to show you this important aspect now in more detail. The Integration block defines the Lambda function invoked when an HTTP request arrives.

"Integration": {
"Type": "AWS",
"IntegrationHttpMethod": "POST",
"Uri": {"Fn::Join" : ["", ["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["Lambda", "Arn"]}, "/invocations"]]},
"IntegrationResponses": [{
"ResponseTemplates": {
"application/json": "$input.json('$.body')"
},
"ResponseParameters": {
"method.response.header.Link": "integration.response.body.headers.next"
},
"StatusCode": 200
}],
"PassthroughBehavior": "NEVER",
"RequestTemplates": {
"application/json": "{\"fun\": \"getUsers\", \"parameters\": {\"limit\": \"$input.params('limit')\", \"next\": \"$input.params('next')\"}}"
}
}

Now it’s time to describe the endpoint that we need to use the REST API.

Describing the Endpoint

"RestApiDeployment": {
"Type": "AWS::ApiGateway::Deployment",
"Properties": {
"RestApiId": {"Ref": "RestApi"},
"StageName": "v1"
},
"DependsOn": ["UsersGet", "UsersPost", "UserGet", "UserDelete", "UserTasksGet", "UserTasksPost", "UserTaskPut", "UserTaskDelete", "CategoryTasksGet"]
}

It’s important that you explicitly depend on all the AWS::ApiGateway::Methods.

Have a look at the following example to see the API in action.

Example

The example in this article reuses the multi-user ToDo application from chapter 10 in Amanzon Web Services in Action. You can find the code for the original example in the book’s code repository.

Setting up

clone the repository

$ git clone git@github.com:AWSinAction/apigateway.git
$ cd apigateway/

create the lambda code file (lambda.zip)

$ npm install --production
$ ./bundle.sh

create an S3 bucket in the US East (N. Virginia, us-east-1) region and upload the lambda.zip file (replace $S3Bucket with a S3 bucket name)

$ export AWS_DEFAULT_REGION=us-east-1
$ export S3Bucket=$(whoami)-apigateway
$ aws s3 mb s3://$S3Bucket
$ aws s3 cp lambda.zip s3://$S3Bucket/lambda.zip

create cloudformation stack

$ aws cloudformation create-stack --stack-name apigateway --template-body file://template_with_api.json --capabilities CAPABILITY_IAM --parameters ParameterKey=S3Bucket,ParameterValue=$S3Bucket

wait until the stack is created (CREATE_COMPLETE)

$ aws cloudformation wait stack-create-complete --stack-name apigateway

get the $ApiId

$ aws cloudformation describe-stacks --stack-name apigateway --query Stacks[0].Outputs

set the $ApiGatewayEndpoint environment variable (replace $ApiId)

export ApiGatewayEndpoint="$ApiId.execute-api.us-east-1.amazonaws.com/v1"

Use the RESTful API

create a user

curl -vvv -X POST -d '{"email": "your@mail.com", "phone": "0123456789"}' -H "Content-Type: application/json" https://$ApiGatewayEndpoint/user

list users

curl -vvv -X GET https://$ApiGatewayEndpoint/user

create a task

curl -vvv -X POST -d '{"description": "test task"}' -H "Content-Type: application/json" https://$ApiGatewayEndpoint/user/$UserId/task

list tasks

curl -vvv -X GET https://$ApiGatewayEndpoint/user/$UserId/task

mark task as complete

curl -vvv -X PUT https://$ApiGatewayEndpoint/user/$UserId/task/$TaskId

delete task

curl -vvv -X DELETE https://$ApiGatewayEndpoint/user/$UserId/task/$TaskId

create a task with a category

curl -vvv -X POST -d '{"description": "test task", "category": "test"}' -H "Content-Type: application/json" https://$ApiGatewayEndpoint/user/$UserId/task

list tasks by category

curl -vvv -X GET https://$ApiGatewayEndpoint/category/$Category/task

Teardown

delete CloudFormation stack

$ aws cloudformation delete-stack --stack-name apigateway

delete S3 bucket (replace $S3Bucket)

$ aws s3 rb --force s3://$S3Bucket

Summary

With API Gateway you can configure a RESTful API. You used a Lambda function to implement the functionality: Each HTTP request invokes a Lambda function. You have very limited overhead to operate your API because you only need to configure your API and implement the functionality. You don’t need to care about servers, scaling, and all the operational overhead.
To automate the configuration of API Gateway you used CloudFormation.

Michael Wittig

Michael Wittig

I’ve been building on AWS since 2012 together with my brother Andreas. We are sharing our insights into all things AWS on cloudonaut and have written the book AWS in Action. Besides that, we’re currently working on bucketAV, HyperEnv for GitHub Actions, and marbot.

Here are the contact options for feedback and questions.