API Nirvana: Building Serverless APIs with AWS SAM and API Gateway šŸŒ (Part 4/5)

Raywall Malheiros
7 min readOct 20, 2023

--

As we move forward in our AWS SAM adventure, we reach a pivotal moment: building APIs with Amazon API Gateway. In this article, weā€™ll guide you through the process of creating RESTful APIs using SAM and API Gateway, allowing you to expose your serverless functions to the world securely and efficiently. Get ready to take your serverless applications to the next level.

I'm assuming you're familiar with the AWS API Gateway from the first episode of our serverless journey. At this point you know what it is and what it's used for. You also know the concept of API, RESTfull API, and BFF. So, let's skip to the funny part, let's build an API Gateway. =)

What have we built until now?

If youā€™ve been following me over the past few weeks and looking at your project, youā€™ll see that weā€™ve built two Lambda functions (hello and user) and a DynamoDB table named UserTable. Now, weā€™ll add a new service to this architecture: an API Gateway to receive requests from our clients that get or set data in our table.

We'll create the API Gateway preparing it for the next episode, where we will implement a lambda authorizer in our project.

Let's start!

Open your previous template.yaml, with contains the lambda and DynamoDB table definitions. In this file, we'll make some changes to start incrementing our template file by creating our API Gateway, resources, methods.

First, let's declare our API Resource using the following code:

# This instruction creates a RESTfull API Gateway Resource
MyGateway:
Type: AWS::Serverless::Api
Properties:
StageName: Prod

As you can see in this above example, it's a really simple instruction, where you'll inform the creation of a resource at the type AWS::Serverless::Api and complement with the StageName.

Next, let's implement our lambda declarations to include reference of our resources and methods. As you can see, this is also a easy adjust, because you'll need just to add a Events property declaring setting the Type API and including the properties Path, Method and RestApiId. The following code is an example of this:

# This instruction creates a Lambda with an Event to link with your API Gateway
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./hello/
Handler: main
Environment:
Variables:
message: "Hello, World!"
Events:
HelloEvent:
Type: Api
Properties:
Path: /hello
Method: GET
RestApiId:
Ref: MyGateway

Note that in the above code, we're saying that our function MyFunction has an Event named HelloEvent and this resource can be called on path /hello and method GET.

Now you know how to config our RESTfull API Gateway, take a time and adjust your template.yaml file. When you finish it'll be probably similar of the following code:

# template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: AWS Serverless Lambda Template

Globals:
Function:
Architectures:
- x86_64
MemorySize: 128
Runtime: go1.x
Timeout: 25

Parameters:
DynamoEndpointUrl:
Type: String
Description: The DynamoDB local URL
Default: 'http://dynamodb:8000'

TableName:
Type: String
Description: The DynamoDB table for storing user information.
Default: 'UserTable'

RegionName:
Type: String
Description: Default region for deployment.
Default: 'sa-east-1'

AwsEnvironmentName:
Type: String
Description: AWS Environment where code is being executed (AWS_SAM_LOCAL or AWS).
Default: 'AWS_SAM_LOCAL'

Resources:
UserTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref TableName
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 2
WriteCapacityUnits: 2

ApiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors:
AllowOrigin: "'*'"

HelloFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./hello/
Handler: main
Environment:
Variables:
message: "Hello, World!"
Events:
HelloEvent:
Type: Api
Properties:
Path: /hello
Method: GET
RestApiId:
Ref: ApiGateway

UserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./users/
Handler: main
Policies: AmazonDynamoDBFullAccess
Environment:
Variables:
AWSENV: !Ref AwsEnvironmentName
ENDPOINT: !Ref DynamoEndpointUrl
REGION: !Ref RegionName
TABLE: !Ref TableName
Events:
PutUser:
Type: Api
Properties:
Path: /user
Method: POST
RestApiId:
Ref: ApiGateway
GetUser:
Type: Api
Properties:
Path: /user/{userId}
Method: GET
RestApiId:
Ref: ApiGateway

I have great news for you! We have just finished all of our changes. =)
What do you think about running some tests?

Running and consuming our API Gateway locally

As we've done before, first thing we'll need is build our project using the following command:

sam build -t template.yaml

If everything is all right, youā€™ll probably see a ā€œBuild Succeededā€ message, and after that weā€™ll be ready to perform our RESTfull API tests, and for this, we'll use a different command of SAM named start-api. Let's do this practice by executing the following command:

sam local start-api --docker-network local-api-network

Look at the above image. As you see, when we start a local-api, SAM has created all of our methods and resources, and now we're ready to do some tests.

You can use a curl command to test your API gateway using the following command:

curl -X GET http://localhost:3000/hello

But, like demostrated in the following image, I've used the postman app to call our API.

To allow the test of different methods in the same lambda function I've changed a little bit our UserFunction lambda code, to identify the method called and respond with the correct action. The following code represent the final version of our lambda function.

You can also use a framework like Gin to help with this. If you're interested in learning more about how to use Gin, Lambda and Go, please leave a comment and I'll write an article about it.

// This is our UserFunction main.go file adapted to detect the method called
package main

import (
"encoding/json"
"fmt"
"log"
"os"

"opa-lambda-database/data"
"opa-lambda-database/models"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
)

var client *dynamodb.DynamoDB

func init() {
sess := session.Must(session.NewSession())
config := aws.NewConfig().WithRegion(os.Getenv("AWS_REGION"))

if os.Getenv("AWSENV") == "AWS_SAM_LOCAL" {
config = config.WithEndpoint(os.Getenv("ENDPOINT"))
}

client = dynamodb.New(sess, config)
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
if request.HTTPMethod == "POST" {
var user models.User
err := json.Unmarshal([]byte(request.Body), &user)

if err != nil {
log.Println("Failed to unmarshal user:", err)

return events.APIGatewayProxyResponse{
Body: fmt.Sprintf("Failed to read record: %s", err.Error()),
StatusCode: 400,
}, nil
}

err = data.Update(client, user)
if err != nil {
log.Println("Failed to insert record:", err)

return events.APIGatewayProxyResponse{
Body: fmt.Sprintf("Failed to read record: %s", err.Error()),
StatusCode: 500,
}, nil
}

return events.APIGatewayProxyResponse{StatusCode: 201}, nil

} else if request.HTTPMethod == "GET" {
userId := request.PathParameters["userId"]
record, err := data.Read(client, userId)

if err != nil {
log.Println("Failed to read record:", err)
return events.APIGatewayProxyResponse{StatusCode: 500}, nil
}

response, _ := json.Marshal(record)
return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{"Content-Type": "application/json"},
Body: string(response),
}, nil
}

return events.APIGatewayProxyResponse{StatusCode: 405}, nil
}

func main() {
lambda.Start(handler)
}

Now we can test the /user resource with GET and POST methods. Let's start by putting a record in DynamoDB with a POST request.

Sounds great! We had a 201 status code, indicanting that we've inserted our new record. Let's use the GET request to retrieve this record and verify that it is correct.

Perfect! Now you have a powerful tool to use your imagination and create functions and low-complexity microservices for your system quickly using your RESTfull API.

Those are some links that you can read and learn more about AWS API Gateway configuration. Also you can take a look at the repository with the complete project on my GitHub profile:

Next week we will continue our journey on serverless world, implementing a lambda authorizer in our infrastructure to improve the security of our API.

Before you go, check out our schedule for the next episodes:

  1. Sept. 29th ā€” Unleash the Power of AWS SAM: A Developerā€™s Guide to Building Serverless Magic with Go
  2. Oct. 6th ā€” Supercharge Your Apps with AWS Lambda: Getting Started with SAM
  3. Oct. 13th ā€” Seamless Data Management with AWS SAM: Integrating DynamoDB
  4. Oct. 20th ā€” API Nirvana: Building Serverless APIs with AWS SAM and API Gateway
  5. Oct. 27th ā€” Locking Down Your Serverless World: Implementing Lambda Authorizers

I hope you liked, and Iā€™ll see you in the next week, bye!

--

--

Raywall Malheiros

Senior Software Engineer and TechLead at ItaĆŗ Unibanco | 4x AWS Certified