Serverless Made Simple — Building an API with AWS API Gateway, AWS Lambda and DynamoDB

Lorenzo Panzeri
8 min readApr 18, 2023

--

Every time I want to build something I always make the same error: starting from scratch. I realize that I have a lot of half started / half finished side projects and one of the causes (beside my full time job 😀 ) is the overhead of searching in older projects or on the internet pieces to glue together and not having some templates or “startup” examples to leverage and optimize my time, achieving the goal faster.

That’s why Ihave built the solution I’m writing about, this is an easy implementation to solve a common problem: publish an API that with a Lambda can interact with another AWS service (DynamoDB in this case)

This solution is fully serverless and represents a solid base for a more complex solution like some logistic management tool, ticketing platform or even an ecommerce.

As usual when we talk about serverless my weapons of choice are AWS SAM and Java. Little off-topic: I switched to IntelliJ IDEA Community Edition because of half a day of my free time was spent understanding why VSCode nuked a project of mine and started to give me errors without touching even a line of code 😟

You will find all this project on my github:

So let’s start with our template.yaml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
PutItem


Serverless template to manage a request that comes from Api Gateway to Lambda and gets stored into DynamoDb

Remember the “Transform” part, a popular question in different AWS Certification exam is how to recognize a SAM Template from a CloudFormation one and the answer is this “Transform” keyword. It means that this template is written using the faster and simpler SAM syntax and that CloudFormation needs to “inflate” it before deploying to our infrastructure 🙂

A common thing to do is leverage the “Globals” on top of the template: you can put there all the common configuration of your resources and avoid to “boilerplating” each one of them:

Globals:
Function:
Timeout: 20
Runtime: java11
Architectures:
- arm64
MemorySize: 512
Environment:
Variables:
JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1

I’m trying to use the arm64 architecture because AWS promoted it so much and it seems to be more efficient and cost effective that x86 one (there is a lot of literature about it, in my opinion there is a reason to worry about the architecture only if you have to handle heavy or stressful loads)

Now let’s speak about resources:

Resources:

#API Gateway created using SAM simple declaration
ItemAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: dev
Name: item-api-gateway

As you may know in SAM templates we can use the SAM “easier” syntax (commonly they start with “AWS::Serverless” or the CloudFormation that may be more configurable and detailed like in this case “AWS::ApiGateway”.

SAM is fully compatible with CloudFormation syntax also because all SAM template will be translated in CloudFormation ones.

Notice the “StageName” property: you can populate it at deploy time using Parameters (put them BEFORE the “Resources” keyword):

Parameters:
ApiGatewayStage:
Type: String

You can pass the value of this parameter when calling the “sam deploy” command (I’ll show you later how).

Let’s finish our little digression about Parameters, you can reference them using the Intrinsic Function “!Ref” like this:

 ItemAPIGateway:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref ApiGatewayStage

Now you can handle different Stages like Development, Staging and Production 😉

(Remember: Friday is not for Production deployment!)

Now the main course, some Lambda definition:

#Lambda function
PutItemLambda:
Type: AWS::Serverless::Function
Properties:
Handler: it.loooop.PutItem::handleRequest
CodeUri: .
Environment:
Variables:
DYNAMODB_ITEMS_TABLE: !Ref ItemsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable
Events:
ApiEvents:
Type: Api
Properties:
Path: /item
Method: POST
RestApiId: !Ref ItemAPIGateway

To correctly understand two of these properties we need to spend a little time speaking also about the “correct” project structure:

This may not be the perfect one but i think that fits our needs 🙂

Look at these two properties:

Handler: it.loooop.PutItem::handleRequest
CodeUri: .

For every Lambda you define you have to point out where the code “starts”. A Lambda is made of two parts (three, if we also count developer’s tears): the code OUTSIDE the handler and the “businesscode INSIDE the handler.

So “Handler” properties get the value of <package_name>.<class_name>::<handler_name>

CodeUri” refers to the position of the pom.xml, in this case it’s only the dot because it’s “here”, in the root of the project.

Lambda execution cycle starts with a first step that is called “Cold start”, when Lambda executes and initializes the resources OUTSIDE the handler. These resources will be reused while the Lambda stays “hot” without need to re-initialize them.

It’s best practice (and also remember it for the Certification exam) to put OUTSIDE the handler the resource declaration like the client for DynamoDB, AWS SNS, RDS connection (this is a more complex topic because RDS connection stalls so you have to check if it’s active in the handler and if needed you have to “refresh” it but this is another story 🙂) etc…

You can leverage the Environment variables to pass to the Lambda some parametric value:

   Environment:
Variables:
DYNAMODB_ITEMS_TABLE: !Ref ItemsTable

Here I’m passing the DynamoDB table where Lambda will put our item.

To work with a DynamoDb table (or everything else) you need to have the right to access:

Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ItemsTable

Here comes the “Policies” keyword. In this case we are giving this Lambda complete control over the Table.

And now we are where the magic happens, how can Lambda integrate with Api Gateway? Catching “Events”:

Events:
ApiEvents:
Type: Api
Properties:
Path: /item
Method: POST
RestApiId: !Ref ItemAPIGateway

This Lambda will answer to the referred (!Ref) Api Gateway (previously declared) when a POST method is called on the “/item” path. Simple and straightforward, isn’t it?

Leveraging “Events” you can define complex Api Gateway + Lambda structure and build scalable and reliable projects!

Last but not least, some persistence:

#DynamoDB table created using SAM simple declaration
ItemsTable:
Type: AWS::Serverless::SimpleTable
Properties:
TableName: items
PrimaryKey:
Name: itemId
Type: String

Simple DynamoDb table. Also on defining DynamoDB keys there are a lot of resources. If you are interested Iwant to suggest you this Alex DeBrie talk:

To close up some “Outputs”:

Outputs:
ItemEndpoint:
Description: API Gateway Endpoint
Value:
Fn::Sub: https://${ItemAPIGateway}.execute-api.${AWS::Region}.amazonaws.com/dev

It’s useful to “print” here the Api Gateway’s endpoint to quickly set up Postman on it for testing (or using the AWS Console)

When working with DynamoDb it’s nice to know that there is the DynamoDbEnhancedClient that provides some more “high level” interaction with the resource. I’m also trying to build some “library” to take with me (and maybe package it as Lambda Layer but also this is another story 😛)

You can use this component in every service that needs to interact with DynamoDb inside you project (you specify the table directly in the service):

Notice that I get the table’s name from the Environment Variable we defined in the AWS SAM template.yaml .

In the “enhanced” built-in functionality there are also some useful annotations:

@DynamoDbBean to declare this class as “table element”, @DynamoDbPartitionKey to point out the chosen PartitionKey.

You can also notice some Jackson annotation because another thing I find useful is to map directly the request body in a POJO using Jackson:

This example may be too “direct”: when you have to deal with complex json request that non direct maps to an item maybe it’s better to decouple the mapping using a PutItemRequest object and after build a mapping function (call it mapper, call it decoupler, you choose).

Few words about the pom.xml:

I find useful to bring inside the BOM of the AWS SDK 2.x and keep all versioning in “properties”

Now it’s time to fire it up!

Some validation with:

sam validate --lint

Now let’s build:

sam build

… and deploy:

sam deploy --guided

I nearly forgot: to set some parameters at deploy time you can use:

sam deploy --guided --parameter-overrides <parameterName>=<parameter_value>

Or you can use the samconfig.toml:

version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "apigw-lambda-dynamodb"
s3_prefix = "apigw-lambda-dynamodb"
region = "eu-west-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
profile = "your-aws-profile"
disable_rollback = true
image_repositories = []
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-1234567890abcde"
parameter-overrides = "ParameterName=\"parameterValue\""

Deploy successful!

Check it out also in CloudFormation:

Time for truth, open Postman:

All went OK, you will find the new record on DynamoDB in AWS Console (itemId is different because i forgot to take the screenshot 😟)

Let’s make a second call to speak about the “Cold start”

JDK Java has this “Cold start problem” that takes a lot of time to start up. The first call took 5 seconds to send back a response BUT the second one only 253 milliseconds. This means that you will face the “cold starts gate” only when the Lambda wakes up from sleep. If the load continues to come or increases, you won’t face this latency until the load dramatically decreases or stops.

There is the GraalVM runtime that allows to improve cold starts and there is also the SnapStart functionality but in my opinion:

if you worry too much about “Cold starts”, serverless is not the correct tool for your solution

Maybe an ECS with a containerized Spring Boot (or Quarkus 🤩) fits better.

And now remember to keep your friends close but your enemies closer… and your AWS Account clean:

sam delete --stack-name apigw-lambda-dynamodb

Hope that this will be useful and jump-start some of yours (and mine) new projects!

See you next time! 😜

--

--

Lorenzo Panzeri

Passionate Developer - Compulsive learner - Messy maker