Serverless CRUD API with NodeJS and Go

Building a serverless API with email notifications in AWS with Terraform — Part 2

Let’s finish our API part of the project by implementing the remaining CRUD methods

Andre Lopes
Level Up Coding
Published in
14 min readDec 14, 2023

--

Photo by Oskar Yildiz on Unsplash

Hey people!

In the first part of our story, we started our project by creating our base infrastructure with a GET endpoint for getting a movie by ID. We created our API Gateway, our first lambda with NodeJS, and our DynamoDB table.

In this second part, we’ll finalize our API endpoints to have a working CRUD application.

We’ll implement our remaining endpoints in other languages to demonstrate how easy and flexible it is to work with Lambdas. Some of the languages are Go and Typescript.

From part 1, we have the following architecture:

In this story, we’ll implement the following part of the architecture:

Requirements

  • An AWS account
  • Any Code Editor of your choice — I use Visual Studio Code
  • NodeJS
  • Go — This will be necessary for the following parts
  • GitHub account — We’ll be using GitHub Actions to deploy our Terraform code

Let’s begin — Implementing Create Movie endpoint

Let’s start by creating the action to create a movie.

The current folder structure from Part 1 should be similar to:

For the new lambda, let’s use Go as the runtime. So, we need to adapt our Lambda module to allow us to define it.

In the variables.tf of the lambda module, let’s add a new variable for the runtime:

variable "runtime" {
description = "The runtime for the Lambda function [nodejs20.x, go1.x]"
type = string
default = "nodejs20.x"
}

variable "name" {
description = "The name of the Lambda function"
type = string
nullable = false
}

variable "handler" {
description = "The handler function in your code for he Lambda function"
type = string
default = "index.handler"
}

variable "init_filename" {
description = "The file containing the initial code for the Lambda"
type = string
default = "index.mjs"
}

For the main.tf :

resource "aws_iam_role" "iam_for_lambda" {
name = "${var.name}-lambda-role"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

resource "aws_lambda_function" "lambda" {
filename = data.archive_file.lambda.output_path
function_name = var.name
role = aws_iam_role.iam_for_lambda.arn
handler = var.handler
runtime = var.runtime
}

And for the datasources.tf, let’s modify our code to calculate the correct file to use, depending on the runtime:

data "archive_file" "lambda" {
type = "zip"
source_file = "./modules/lambda/init_code/${var.init_filename}"
output_path = "lambda_function_payload.zip"
}

data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}

Note that we use locals to define which init code will be used depending on the runtime. You already have the index.js file in the init_code folder. You can use the already-built Go main file in this repository. Or you can compile your own with the following code:

package main

import (
"context"
"github.com/aws/aws-lambda-go/lambda"
)

type MyEvent struct {
Name string `json:"name"`
}

type Response struct {
Body string `json:"body"`
StatusCode int `json:"statusCode"`
}

func HandleRequest(ctx context.Context, event *MyEvent) (*Response, error) {
message := Response{
Body: "Hello from Lambda!",
StatusCode: 200,
}
return &message, nil
}

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

You can find the code and build instructions here.

Now, let’s add our module to the iac/lambdas.tf file:

module "create_movie_lambda" {
source = "./modules/lambda"
name = "create-movie"
runtime = "go1.x"
handler = "main"
}

To see a full list of all supported runtimes, check the documentation here.

Let’s also give our lambda permissions to add items to our table. In the iac/iam-policies.tf add:

data "aws_iam_policy_document" "put_movie_item" {
statement {
effect = "Allow"

actions = [
"dynamodb:PutItem",
]

resources = [
aws_dynamodb_table.movies-table.arn
]
}
}

resource "aws_iam_policy" "put_movie_item" {
name = "put_movie_item"
path = "/"
description = "IAM policy allowing PUT Item on Movies DynamoDB table"
policy = data.aws_iam_policy_document.put_movie_item.json
}

resource "aws_iam_role_policy_attachment" "allow_putitem_create_movie_lambda" {
role = module.create_movie_lambda.role_name
policy_arn = aws_iam_policy.put_movie_item.arn
}

We need to add a new method to our iac/rest-api.tf file to link it to our lambda:

module "create_movie_method" {
source = "./modules/rest-api-method"
api_id = aws_api_gateway_rest_api.movies_api.id
http_method = "POST"
resource_id = aws_api_gateway_resource.movies_root_resource.id
resource_path = aws_api_gateway_resource.movies_root_resource.path
integration_uri = module.create_movie_lambda.invoke_arn
lambda_function_name = module.create_movie_lambda.name
region = var.region
account_id = var.account_id
}

And then add the create_movie_method configuration to our deployment resource in the same file:

resource "aws_api_gateway_deployment" "movies_api_deployment" {
rest_api_id = aws_api_gateway_rest_api.movies_api.id

triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.movies_root_resource.id,
aws_api_gateway_resource.movie_resource.id,
module.get_movie_method.id,
module.get_movie_method.integration_id,
module.create_movie_method.id,
module.create_movie_method.integration_id,
]))
}

lifecycle {
create_before_destroy = true
}
}

Now push the code to GitHub and see the lambda, and API be created:

New API endpoint
Lambda functions

You can test it by making a POST HTTP request to /movies, you should get a response with status 200 and a body like this:

Hello from Lambda!

Now, we need to code our lambda.

In the folder apps, create a new folder named create-movie. Navigate to the folder and run the following code to initialize a new go module:

go init example-movies.com/create-movie

Then, run the following code to get the necessary packages we’ll require:

go get "github.com/aws/aws-lambda-go"
go get "github.com/aws/aws-sdk-go"
go get "github.com/google/uuid"

Now, let’s set our models in a models.go file:

package main

type Request struct {
Title string `json:"title"`
Rating float64 `json:"rating"`
Genres []string `json:"genres"`
}

type Response struct {
ID string `json:"id"`
Title string `json:"title"`
Rating float64 `json:"rating"`
Genres []string `json:"genres"`
}

type ErrorResponse struct {
Message string `json:"message"`
}

type Movie struct {
ID string `dynamodbav:",string"`
Title string `dynamodbav:",string"`
Genres []string `dynamodbav:",stringset"`
Rating float64 `dynamodbav:",number"`
}

And then for our Lambda, a simple, straightforward implementation:

package main

import (
"context"
"encoding/json"

"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"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"github.com/google/uuid"
)

func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
var newMovie Request
err := json.Unmarshal([]byte(request.Body), &newMovie)

if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error marshalling new movie item, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))

// Create DynamoDB client
dynamoDbService := dynamodb.New(sess)

item := Movie{
ID: uuid.NewString(),
Title: newMovie.Title,
Genres: newMovie.Genres,
Rating: newMovie.Rating,
}

av, err := dynamodbattribute.MarshalMap(item)
if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error marshalling new movie item to DynamoAttribute, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

// Create item in table Movies
tableName := "Movies"

input := &dynamodb.PutItemInput{
Item: av,
TableName: aws.String(tableName),
}

_, err = dynamoDbService.PutItem(input)
if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error calling PutItem, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

responseData := Response{
ID: item.ID,
Title: item.Title,
Genres: item.Genres,
Rating: item.Rating,
}

responseBody, err := json.Marshal(responseData)

response := events.APIGatewayProxyResponse{
Body: string(responseBody),
StatusCode: 200,
}

return response, nil
}

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

Deploying the lambda

We now have our lambda and infrastructure ready. It is time to deploy it.

In the .github/workflows folder, create a new file named deploy-create-movie-lambda.yml. In it, add the following workflow code to build and deploy our Go lambda:

name: Deploy Create Movie Lambda
on:
push:
branches:
- main
paths:
- apps/create-movie/**/*
- .github/workflows/deploy-create-movie-lambda.yml

defaults:
run:
working-directory: apps/create-movie/

jobs:
terraform:
name: "Deploy CreateMovie Lambda"
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3

- uses: actions/setup-go@v4.1.0
with:
go-version: "1.21.4"

- name: Configure AWS Credentials Action For GitHub Actions
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-central-1 # Set your region here

- name: Build Lambda
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/ .

# The lambda requires that the executing file be named "main"
- name: Rename file
run: mv ./build/create-movie ./build/main

- name: Zip build
run: zip -r -j main.zip ./build

- name: Update Lambda code
run: aws lambda update-function-code --function-name=create-movie --zip-file=fileb://main.zip

Don’t forget to change the aws-region to your region

Once you deploy it, you can send POST request to /movies with a similar body:

{
"title": "Starship Troopers",
"genres": ["Action", "Sci-Fi"],
"rating": 7.3
}

And you should get a similar response:

{
"id": "4e70fef6-d9cc-4056-bf9b-e513cdabc69f",
"title": "Starship Troopers",
"rating": 7.3,
"genres": [
"Action",
"Sci-Fi"
]
}

Great! We have our endpoints to get a movie and to create a movie. Let’s go to our next step and create one to delete a movie.

Deleting a movie

Now let’s add our module to the iac/lambdas.tf file:

module "delete_movie_lambda" {
source = "./modules/lambda"
name = "delete-movie"
runtime = "nodejs20.x"
handler = "index.handler"
}

Let’s also give our lambda permissions to delete items from our table. In the iac/iam-policies.tf add:

data "aws_iam_policy_document" "delete_movie_item" {
statement {
effect = "Allow"

actions = [
"dynamodb:DeleteItem",
]

resources = [
aws_dynamodb_table.movies-table.arn
]
}
}

resource "aws_iam_policy" "delete_movie_item" {
name = "delete_movie_item"
path = "/"
description = "IAM policy allowing DELETE Item on Movies DynamoDB table"
policy = data.aws_iam_policy_document.delete_movie_item.json
}

resource "aws_iam_role_policy_attachment" "allow_deleteitem_delete_movie_lambda" {
role = module.delete_movie_lambda.role_name
policy_arn = aws_iam_policy.delete_movie_item.arn
}

We need to add a new method to our iac/rest-api.tf file to link it to our lambda:

module "delete_movie_method" {
source = "./modules/rest-api-method"
api_id = aws_api_gateway_rest_api.movies_api.id
http_method = "DELETE"
resource_id = aws_api_gateway_resource.movie_resource.id
resource_path = aws_api_gateway_resource.movie_resource.path
integration_uri = module.delete_movie_lambda.invoke_arn
lambda_function_name = module.delete_movie_lambda.name
region = var.region
account_id = var.account_id
}

And then add the delete_movie_method configuration to our deployment resource in the same file:

resource "aws_api_gateway_deployment" "movies_api_deployment" {
rest_api_id = aws_api_gateway_rest_api.movies_api.id

triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.movies_root_resource.id,
aws_api_gateway_resource.movie_resource.id,
module.get_movie_method.id,
module.get_movie_method.integration_id,
module.create_movie_method.id,
module.create_movie_method.integration_id,
module.delete_movie_method.id,
module.delete_movie_method.integration_id,
]))
}

lifecycle {
create_before_destroy = true
}
}

Now push the code to GitHub and see the lambda, and API be created:

And the lambda:

Coding the lambda in Typescript

For our delete movie lambda, I want to show you how easily it is to use Typescript to develop it.

Let’s do as before and create a new folder under apps named delete-movie, navigate to it in the terminal and run the following script to initialize our npm project:

npm init -y

Now let’s add Typescript with:

npm i -D typescript

And then add the property type with value module and a new npm script named tsc with the code tsc :

{
"name": "get-movie",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"tsc": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",,
"devDependencies": {
"typescript": "^5.3.3"
}
}

Run the following code to start our Typescript project and generate a typescript.json file:

npm run tsc -- --init --target esnext --module nodenext \
--moduleResolution nodenext --rootDir src \
--outDir build --noImplicitAny --noImplicitThis --newLine lf \
--resolveJsonModule

If you are on Windows, run the following:

npm run tsc -- --init --target esnext --module nodenext `
--moduleResolution nodenext --rootDir src `
--outDir build --noImplicitAny --noImplicitThis --newLine lf `
--resolveJsonModule

Now let’s add our dependencies:

npm i -s @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb aws-sdk
npm i -D @types/aws-lambda copyfiles

Great!

Now for our lambda implementation code, create a src folder and then an index.ts file with the following code:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

const tableName = "Movies";

export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const movieID = event.pathParameters?.movieID;

if (!movieID) {
return {
statusCode: 400,
body: JSON.stringify({
message: "Movie ID missing",
}),
};
}

console.log("Deleting movie with ID ", movieID);

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

const command = new DeleteCommand({
TableName: tableName,
Key: {
ID: movieID.toString(),
},
});

try {
await docClient.send(command);

return {
statusCode: 204,
body: JSON.stringify({
message: `Movie ${movieID} deleted`,
}),
};
} catch (e: any) {
console.log(e);

return {
statusCode: 500,
body: JSON.stringify({
message: e.message,
}),
};
}
};

Now we just need to add our build npm script and our deploy workflow.

For the build script, add a new npm script named build:

{
"name": "get-movie",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"tsc": "tsc",
"build": "tsc && copyfiles package.json build/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.470.0",
"@aws-sdk/lib-dynamodb": "^3.470.0",
"aws-sdk": "^2.1515.0"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.130",
"copyfiles": "^2.4.1",
"typescript": "^5.3.3"
}
}

Again, we copy our package.json file to let our Lambda runtime know about our project configurations. Also, if you need extra packages, you might need to download your node packages before and ship it to your lambda with your main code.

Now for the GitHub actions workflow, create a deploy-delete-movie-lambda.yml file in the .github/workflows folder with the code:

name: Deploy Delete Movie Lambda
on:
push:
branches:
- main
paths:
- apps/delete-movie/**/*
- .github/workflows/deploy-delete-movie-lambda.yml

defaults:
run:
working-directory: apps/delete-movie/

jobs:
terraform:
name: "Deploy DeleteMovie Lambda"
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3

- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: 20

- name: Configure AWS Credentials Action For GitHub Actions
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-central-1

- name: Install packages
run: npm install

- name: Build
run: npm run build

- name: Zip build
run: zip -r -j main.zip ./build

- name: Update Lambda code
run: aws lambda update-function-code --function-name=delete-movie --zip-file=fileb://main.zip

Push your code to GitHub and wait for it to succeed.

Now, as the deployed code is Javascript, you can see it in the delete-movie lambda.

You can make a DELETE HTTP request to the /movies/{movieID} URL and receive a 204 status code. Then you can check the Movies table in DynamoDB to see that your record was deleted.

Awesome!

Now let’s dive into updating a movie

Updating a movie

As the code will be similar, let’s also build this one using Go.

Let’s add our infrastructure to the iac/lambdas.tf file:

module "update_movie_lambda" {
source = "./modules/lambda"
name = "update-movie"
runtime = "go1.x"
handler = "main"
}

Let’s also give our lambda permissions to update items from our table. In the iac/iam-policies.tf add:

data "aws_iam_policy_document" "update_movie_item" {
statement {
effect = "Allow"

actions = [
"dynamodb:UpdateItem",
]

resources = [
aws_dynamodb_table.movies-table.arn
]
}
}

resource "aws_iam_policy" "update_movie_item" {
name = "update_movie_item"
path = "/"
description = "IAM policy allowing UPDATE Item on Movies DynamoDB table"
policy = data.aws_iam_policy_document.update_movie_item.json
}

resource "aws_iam_role_policy_attachment" "allow_updateitem_update_movie_lambda" {
role = module.update_movie_lambda.role_name
policy_arn = aws_iam_policy.update_movie_item.arn
}

We need to add a new method to our iac/rest-api.tf file to link it to our lambda:

module "update_movie_method" {
source = "./modules/rest-api-method"
api_id = aws_api_gateway_rest_api.movies_api.id
http_method = "PUT"
resource_id = aws_api_gateway_resource.movie_resource.id
resource_path = aws_api_gateway_resource.movie_resource.path
integration_uri = module.update_movie_lambda.invoke_arn
lambda_function_name = module.update_movie_lambda.name
region = var.region
account_id = var.account_id
}

And then add the update_movie_method configuration to our deployment resource in the same file:

resource "aws_api_gateway_deployment" "movies_api_deployment" {
rest_api_id = aws_api_gateway_rest_api.movies_api.id
triggers = {

redeployment = sha1(jsonencode([
aws_api_gateway_resource.movies_root_resource.id,
aws_api_gateway_resource.movie_resource.id,
module.get_movie_method.id,
module.get_movie_method.integration_id,
module.create_movie_method.id,
module.create_movie_method.integration_id,
module.delete_movie_method.id,
module.delete_movie_method.integration_id,
module.update_movie_method.id,
module.update_movie_method.integration_id,
]))
}

lifecycle {
create_before_destroy = true
}
}

Now push the code to GitHub and see the lambda, and API be created:

And the lambdas:

Now, test the integration by making a PUT HTTP request to /movies/{movieID} and you should get back a 200 status code with:

Hello from Lambda!

Implementing the lambda code

In the folder apps, create a new folder named update-movie. Navigate to the folder and run the following code to initialize a new go module:

go init example-movies.com/update-movie

Then, run the following code to get the necessary packages we’ll require:

go get "github.com/aws/aws-lambda-go"
go get "github.com/aws/aws-sdk-go"

Now, let’s set our models in a models.go file:

package main

type Request struct {
Title string `json:"title"`
Rating float64 `json:"rating"`
Genres []string `json:"genres"`
}

type ErrorResponse struct {
Message string `json:"message"`
}

type MovieData struct {
Title string `dynamodbav:":title,string" json:"title"`
Genres []string `dynamodbav:":genres,stringset" json:"genres"`
Rating float64 `dynamodbav:":rating,number" json:"rating"`
}

Note that we could create a shared module to reuse some of the Go code, but for the sake of simplicity, we are repeating the code here.

And then for our Lambda, a simple straightforward implementation:

package main

import (
"context"
"encoding/json"
"strings"

"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"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
movieID := request.PathParameters["movieID"]

if strings.TrimSpace(movieID) == "" {
response, _ := json.Marshal(ErrorResponse{
Message: "Movie ID invalid",
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 400,
}, nil
}

var updateMovie Request
err := json.Unmarshal([]byte(request.Body), &updateMovie)

if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error marshalling update movie item, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))

// Create DynamoDB client
dynamoDbService := dynamodb.New(sess)

movie := MovieData{
Title: updateMovie.Title,
Genres: updateMovie.Genres,
Rating: updateMovie.Rating,
}

attributeMapping, err := dynamodbattribute.MarshalMap(movie)

if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error marshalling update movie item to DynamoAttribute, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

// Create item in table Movies
tableName := "Movies"

input := &dynamodb.UpdateItemInput{
ExpressionAttributeValues: attributeMapping,
TableName: aws.String(tableName),
Key: map[string]*dynamodb.AttributeValue{
"ID": {
S: aws.String(movieID),
},
},
ReturnValues: aws.String("UPDATED_NEW"),
UpdateExpression: aws.String("set Rating = :rating, Title = :title, Genres = :genres"),
}

_, err = dynamoDbService.UpdateItem(input)
if err != nil {
response, _ := json.Marshal(ErrorResponse{
Message: "Got error calling UpdateItem, " + err.Error(),
})

return events.APIGatewayProxyResponse{
Body: string(response),
StatusCode: 500,
}, nil
}

response := events.APIGatewayProxyResponse{
StatusCode: 200,
}

return response, nil
}

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

And now to deploy it, in the .github/workflows, create a new deploy-update-movie-lambda.yml file and add the following code:

name: Deploy Update Movie Lambda
on:
push:
branches:
- main
paths:
- apps/update-movie/**/*
- .github/workflows/deploy-update-movie-lambda.yml

defaults:
run:
working-directory: apps/update-movie/

jobs:
terraform:
name: "Deploy UpdateMovie Lambda"
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3

- uses: actions/setup-go@v4.1.0
with:
go-version: "1.21.4"

- name: Configure AWS Credentials Action For GitHub Actions
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-central-1

- name: Build Lambda
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/ .

# The lambda requires that the executing file be named "main"
- name: Rename file
run: mv ./build/update-movie ./build/main

- name: Zip build
run: zip -r -j main.zip ./build

- name: Update Lambda code
run: aws lambda update-function-code --function-name=update-movie --zip-file=fileb://main.zip

Now, push it to GitHub and wait for the workflow to succeed.

To test, send a PUT request to /movies/{movieID} with a similar body example below:

{
"title": "Jurassic Park",
"rating": 8.2,
"genres": [
"Action",
"Adventure",
"Sci-Fi",
"Thriller"
]
}

Conclusion

We’ve successfully built a serverless CRUD API with NodeJS and Go on AWS using Terraform. We started by setting up the foundational components in Part 1, including an API Gateway, Lambda functions, and a DynamoDB table. In this article, we implemented CRUD operations by adding functionalities for creating, updating, and deleting movie records.

The flexibility of AWS Lambda allowed us to showcase the ease of working with different programming languages, demonstrating implementations in both Go and TypeScript. Each Lambda function was equipped with the necessary permissions to interact with the DynamoDB table, showcasing the power of AWS Identity and Access Management (IAM) for secure resource access.

Additionally, we automated the deployment process using GitHub Actions, ensuring a seamless integration of code changes into the AWS environment. By following these steps, readers can replicate and extend the project, gaining insights into building robust serverless APIs with diverse language support.

This journey provided a practical guide to constructing serverless APIs and highlighted the simplicity and scalability achieved by combining AWS services and infrastructure as code (IAC) with Terraform.

The repository for this code can be found here.

Check the part 3 here.

Happy coding! 💻

--

--