Build a Go API

with AWS Lambda functions and Serverless

Stephan Bakkelund Valois
24SevenOffice Tech Blog
9 min readOct 26, 2022

--

Photo by david latorre romero on Unsplash

Go is the undisputed champion when it comes to building fast, distributed, and concurrent systems in the cloud. I’d like to discuss how we can use the Go language, AWS lambdas, and the serverless framework to create a complete (demo) application in the cloud. This will be a lengthy tutorial, so fill up your cup, and let’s dive into it.

The application we’re making is a CRUD API where we can register notes, search up notes, look at a single note, and delete a note. A note can have a header and some content.

A lambda function can be invoked in many different ways. A lambda can be invoked by some sort of event, like an upload to S3, a cron job, an HTTP call, and so forth. As we’re building an API, we’d like the functions to be invoked by an HTTP call.

Now, we could write a monolithic application and jam the whole thing in a single lambda function. This wouldn’t be very distributed though. Instead, we’ll discuss how we may create a single lambda function for each route. This means each route will be its own little application. Wouldn’t that mean we’d be in danger of having a lot of duplicated code across the lambda applications?

One great thing about Go is its ability to build multiple binaries from a single source code. This means we can have shared code between our lambdas, and only the code that we use in our lambdas will get included in the final binary.

You can find the whole repository at https://github.com/ScopeSV/go-notes-lambda-distributed

Table of contents

Setup

The file structure is going to be like this:

cmd/
- delete/
- findAll/
- findOne/
- post/
pkg/
go.mod

Our lambda functions will reside in the cmd directory, while the pkg directory will be used for our reusable code.

To follow along with this tutorial, make sure that you have:

I’m running this on a macOS, so if you run into any problems on Linux or Windows, that might be why — Try Googling and the solution may appear before you!

We are going to start by initiating a new module. In Go, a module is an actual application or library. It may consist of multiple packages, and will also have a go.mod and go.sum file. If you’re familiar with JavaScript and NPM, this is similar to package.json and package-lock.json .

Go package is just a way to namespace a collection of source files that belong together.

Create your project directory, and initialize a new Go project:

$ mkdir notes
$ cd notes && go mod init example.com/notes
go: creating new go.mod: module example.com/notes
go: to add module requirements and sums:
go mod tidy

We created a new directory and initialized a new Go project. The go.mod file will keep track of your application’s metadata, like versions, dependencies, and so on. The main.go file is our application's entry point.

Find all notes

We’ll start by defining our first route. It’s going to be the route that will get all notes. Our notes will be fetched from a DynamoDB database.

$ go get github.com/aws/aws-lambda-go/lambda
$ mkdir -p cmd/findAll && touch cmd/findAll/main.go

As we’re going to put our application in a lambda, we’ll install AWS’s SDK for lambdas. We’ll also create our first endpoint — cmd/findAll/main.go

Let’s write up our main.go file’s structure:

cmd/findAll/main.go

The main function is the application entry point. All the main function is going to do, is start our lambda handler findAll

A note will consist of a title and some content. As we want to distinguish notes from each other, we’ll also give each note a UUID ID. We should define a structure for notes. As the note structure will be the same across all our lambda functions, we should probably look into making the note struct reusable. From the root of our project, we’ll create our pkg directory which will hold our reusable packages. Inside it, we’ll add a structs package:

~notes/ $ mkdir -p pkg/structs/ && touch pkg/structs/notes.go

And write it up:

pkg/structs/notes.go

We have two structs here. NotePayload consists of a title and content. This will be the payload a user will send to our API. Note struct uses the NotePayload fields, and also adds a new ID field to it.

Each DynamoDB operation is going to involve a bit of boilerplate. As we are going to work with our database in every lambda function, it would be smart to create a reusable package of this as well. From the root of our project:

$ mkdir -p pkg/utils/ && touch pkg/utils/dynamodb.go
$ go get github.com/aws/aws-sdk-go-v2/aws
$ go get github.com/aws/aws-sdk-go-v2/config
$ go get github.com/aws/aws-sdk-go-v2/service/dynamodb

And our file:

pkg/utils/dynamodb.go

We write up two functions, which together will load and initialize a dynamo DB client that we can use to fire off database calls. As the GetDynamoClient function is capitalized, it will be exported so that we can import it wherever we want.

Let’s head back to ourfindAll lambda.

cmd/findAll/main.go

We are putting both our reusable code snippets to use here. We create a DynamoDB client and apply the Scan function. The Scan function will get all entities from the specified table. We are passing in an environment variable here, as we’ll define the table name in our Serverless configuration soon.

Go is very explicit with its error handling. As errors are first-class citizens, we check to see if we have any errors, and continue forward if not.

We initialize a variable of type slice of Note struct. A slice works as a dynamic array. Next, we unmarshal the items in our DynamoDB table into our struct. We then parse our struct to JSON and return it to our user.

We need to add some more libraries to our project. Now, instead of getting each one of the dependencies one by one, we can utilize a Go function called tidy. At the root of the project.

$ go mod tidy
go: downloading GitHub.com..
go: added ....

go mod tidy ensures that the go.mod file matches the source code in the module. It adds any missing module requirements necessary to build the current module’s packages and dependencies, and it removes requirements on modules that don’t provide any relevant packages. It also adds any missing entries togo.sum and removes unnecessary entries.
- Official Go Docs

Serverless

We can not just run this program like any other program, as it’s made for running in a lambda environment. There are ways to run this offline, but that’s outside the scope of this blog post. Instead, we’ll look into how we can deploy and run it on AWS lambda via the Serverless framework. At this point, make sure you have Serverless installed.

In the root of our notes project:

$ touch serverless.yml

Lets write some yaml!

serverless.yml

Ok. That was a lot. Let’s spend a minute looking at what’s going on here.

We start by defining the name of this service, and we explicitly tell it that we will package our serverless as individual. This means we for example need to handle how we package and upload each of our lambda functions ourselves. It’s not necessarily a lot more job, but it gives us a bit more control.

Those who have worked with AWS previously know how important IAM roles are. AWS has everything blacklisted, and it’s up to us to whitelist services that can work together. We tell AWS that everything may access our DynamoDB database. In production, you may want to look into how you want to restrict this.

The environment property is where you’d add all your environment variables.

The function property is where you’d define all lambda functions. By now, we only have one function findAll. We tell it which runtime, what the handler’s name is, that it should be invoked on an HTTP GET event, and where to find the packaged artifact.

Makefile

How should we bundle everything together? Your best bet is to write a Makefile. I’m not entirely sure how a Makefile would work on Windows. You may try git bash or WSL. Or else you’d just have to do the steps manually.

We create our Makefile in the root of our project, and write it up:

Makefile

Our main function is deploy. We split the rest of the logic into functions, and call them all from our deploy function. Each time we run our deploy function, we want to build our binaries, zip them, deploy them, and clean up after.

Make sure you are logged into AWS through the terminal in one way or another. Though env variables, AWS credential file, etc. We are about to run our Makefile!

$ make deploy
- Building binaries...
Finished building binaries
- Zipping files...
adding: findAll (deflated 57%)
Finished zipping files
- Deploying to AWS...
Deploying notes to stage dev (eu-west-1)⠇ Uploading (6.73 MB) (3s)

We are running through all the steps! At the end of the process, serverless will print out an endpoint URL to our lambda function. If you paste it into your browser, you’d get an empty array in return. Which is good. We haven’t written any notes yet. Let’s look into that next!

Post a new note

Our post lambda function will take a payload of a note title and content, and put it into DynamoDB.

$mkdir -p cmd/post && touch cmd/post/main.go

We write our post code:

cmd/post/main.go

If you notice, we’ve used a function utils.GenerateUUID() that we haven’t written yet. As this is a typically shared library function, we’ll add it to our utils package.

$ touch pkg/utils/generateUUID.go
pkg/utils/generateUUID.go

Notice that all our GenerateUUID function is doing, is wrapping around Google’s own UUID library’s UUID generator. This wasn’t strictly necessary, however, I wanted to do it this way as a discussion on separating code into its own packages

That should be it for this application to receive HTTP requests. We have to do some slight changes to our Makefile and Serverless file:

// Makefile....@GOOS=linux GOARCH=amd64 go build -o bin/post cmd/post/main.go....// serverless.yml
// underneath functions
post:
runtime: go1.x
handler: post
events:
- http:
path: notes
method: POST
package:
artifact: bin/post.zip

After running our make function, we can try hitting our post route:

$ curl -d '{"title": "Foo", "content": "Bar"}' -X POST https://your-api.execute-api.eu-west-1.amazonaws.com/dev/notes$ curl https://your-api.execute-api.eu-west-1.amazonaws.com/dev/notes
[{"ID":"84054de3-851b-48eb-9faf-00e48a254df9","Title":"Foo","Content":"Bar"}]%

Ok, so this is pretty sweet. We posted a note. We then tried to retrieve all our notes. As we can see, we received an array with our freshly created note in it. It even has an ID. Awesome.

Find a note

$ mkdir -p cmd/findOne && touch cmd/findOne/main.go

New file! New opportunities! Let’s get one note from our database.

cmd/findOne/main.go

We get the ID of the requested note in our URL parameter. We extract it and fetch the note from the database. We put the result into a new Note struct, marshal it to JSON, and send it to the user.

Let’s add this application to both our Makefile and serverless.yml

// Makefile....@GOOS=linux GOARCH=amd64 go build -o bin/findOne cmd/findOne/main.go....// serverless.yml
// underneath functions
findOne:
runtime: go1.x
handler: findOne
events:
- http:
path: notes/{id}
method: GET
package:
artifact: bin/findOne.zip

And run the make deploy script. Now, after the application is successfully deployed, we can try to get the note we previously added. If you’re unsure what the ID is, try doing the fetchAll call again.

curl https://your-api.execute-api.eu-west-1.amazonaws.com/dev/notes/84054de3-851b-48eb-9faf-00e48a254df9
{"ID":"84054de3-851b-48eb-9faf-00e48a254df9","Title":"Foo","Content":"Bar"}%

As we can see, the array is gone. We only got one note. Awesome.

Delete a note

Last, let's create an application that can delete elements. Same procedure as before:

$ mkdir cmd/deleteOne && touch cmd/deleteOne/main.go

And we populate it with the code:

cmd/deleteOne/main.go

Same as before. We collect the note ID from the URL parameter and delete the note from our database. Adding to our serverless and Makefile:

// Makefile....@GOOS=linux GOARCH=amd64 go build -o bin/deleteOne cmd/deleteOne/main.go....// serverless.yml
// underneath functions
deleteOne:
runtime: go1.x
handler: deleteOne
events:
- http:
path: notes/{id}
method: DELETE
package:
artifact: bin/deleteOne

And after a successful deployment:

$ curl -X DELETE https://your-api.execute-api.eu-west-1.amazonaws.com/dev/notes/84054de3-851b-48eb-9faf-00e48a254df9                                                                                                                   
Deleted item%

Did it work? Let’s try to get all notes:

$ curl https://your-api.execute-api.eu-west-1.amazonaws.com/dev/notes/                                                                                                                                                                 
[]%

Seems like it! The note is gone.

Last words

So there you have it. A full (demo) application, distributed among lambda functions.

You can find the whole repository at https://github.com/ScopeSV/go-notes-lambda-distributed

I hope you found it informative, and that it may help you on your way to writing more Go code.

Until next time
Stephan Bakkelund Valois

--

--