API Nirvana: Building Serverless APIs with AWS SAM and API Gateway š (Part 4/5)
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:
- Sept. 29th ā Unleash the Power of AWS SAM: A Developerās Guide to Building Serverless Magic with Go
- Oct. 6th ā Supercharge Your Apps with AWS Lambda: Getting Started with SAM
- Oct. 13th ā Seamless Data Management with AWS SAM: Integrating DynamoDB
- Oct. 20th ā API Nirvana: Building Serverless APIs with AWS SAM and API Gateway
- Oct. 27th ā Locking Down Your Serverless World: Implementing Lambda Authorizers
I hope you liked, and Iāll see you in the next week, bye!