Building a URL Shortener with Go and AWS Lambda

Tin Rabzelj
Tin Rabzelj13 March, 2018

This article shows how to build a URL shortener in Go and deploy it to AWS Lambda. If you're only interested in the source code, you can find it on GitHub.

Getting started

To begin, install Go, if you haven't already. Then Install and configure AWS CLI tool.

Create a directory for your project inside $GOPATH. All of the following paths will be relative to this directory.

The AWS region is assumed to be us-east-1.

DynamoDB table

Create urlshortenerlinks_table.json file, which describes the table.

{
  "TableName": "UrlShortenerLinks",
  "AttributeDefinitions": [{
    "AttributeName": "short_url",
    "AttributeType": "S"
  }],
  "KeySchema": [{
    "KeyType": "HASH",
    "AttributeName": "short_url"
  }],
  "ProvisionedThroughput": {
    "WriteCapacityUnits": 5,
    "ReadCapacityUnits": 5
  }
}

Run the following command to create the table on AWS.

$ aws dynamodb create-table --region us-east-1 --cli-input-json file://urlshortenerlinks_table.json

Shorten function

Create main.go file inside shorten directory.

package main

const (
  LinksTableName = "UrlShortenerLinks"
  Region         = "us-east-1"
)

Declare request and response structs.

type Request struct {
  URL string `json:"url"`
}

type Response struct {
  ShortURL string `json:"short_url"`
}

Declare a struct to describe the DynamoDB table item. ShortURL will be used as primary key.

type Link struct {
  ShortURL string `json:"short_url"`
  LongURL  string `json:"long_url"`
}

Declare Lambda handler function and hook it up in the main function.

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  // ...
}

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

Inside Handler function parse the request body.

rb := Request{}
if err := json.Unmarshal([]byte(request.Body), &rb); err != nil {
  return events.APIGatewayProxyResponse{}, err
}

Start a DynamoDB session.

sess, err := session.NewSession(&aws.Config{
  Region: aws.String(Region),
})
if err != nil {
  return events.APIGatewayProxyResponse{}, err
}
svc := dynamodb.New(sess)

Generate a short URL using teris-io/shortid package. It's called a "URL" even though it's just an ID. The full URL will depend on the hostname.

shortURL := shortid.MustGenerate()
// Because "shorten" endpoint is reserved
for shortURL == "shorten" {
  shortURL = shortid.MustGenerate()
}
link := &Link{
  ShortURL: shortURL,
  LongURL:  rb.URL,
}

Marshal the link into a attribute value map.

av, err := dynamodbattribute.MarshalMap(link)
if err != nil {
  return events.APIGatewayProxyResponse{}, err
}

Insert link into DynamoDB table.

input := &dynamodb.PutItemInput{
  Item:      av,
  TableName: aws.String(LinksTableName),
}
if _, err = svc.PutItem(input); err != nil {
  return events.APIGatewayProxyResponse{}, err
}

Return short URL in the response body.

response, err := json.Marshal(Response{ShortURL: shortURL})
if err != nil {
  return events.APIGatewayProxyResponse{}, err
}
return events.APIGatewayProxyResponse{
  StatusCode: http.StatusOK,
  Body:       string(response),
}, nil

Redirect function

Create another main.go file, this time inside redirect directory.

package main

const (
  LinksTableName = "UrlShortenerLinks"
  Region         = "us-east-1"
)

type Link struct {
  ShortURL string `json:"short_url"`
  LongURL  string `json:"long_url"`
}

Declare Handler function.

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  // ...
}

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

Get short_url parameter inside Handler function.

shortURL, ok := request.PathParameters["short_url"]

Start DynamoDB session.

sess, err := session.NewSession(&aws.Config{
  Region: aws.String(Region),
})
if err != nil {
  return events.APIGatewayProxyResponse{}, err
}
svc := dynamodb.New(sess)

Read link item from DynamoDB table.

result, err := svc.GetItem(&dynamodb.GetItemInput{
  TableName: aws.String(LinksTableName),
  Key: map[string]*dynamodb.AttributeValue{
    "short_url": {
      S: aws.String(shortURL),
    },
  },
})
if err != nil {
  return events.APIGatewayProxyResponse{}, err
}

Parse link item into the Link struct.

link := Link{}
if err := dynamodbattribute.UnmarshalMap(result.Item, &link); err != nil {
  return events.APIGatewayProxyResponse{}, err
}

Redirect user to the long URL by specifying the location header.

return events.APIGatewayProxyResponse{
  StatusCode: http.StatusPermanentRedirect,
  Headers: map[string]string{
    "location": link.LongURL,
  },
}, nil

Deploy

You need to create a IAM role. Go to AWS IAM Management Console, under Roles click on Create role. Then add AmazonDynamoDBFullAccess and AWSXrayFullAccess policies.

IAM Role

Note the Role ARN value.

Build both Go apps on Linux and macOS with the following commands.

$ GOOS=linux GOARCH=amd64 go build -o shorten main.go
$ zip deployment.zip shorten

And if you're on Windows, you'll need to use build-lambda-zip tool.

$ go get -u github.com/aws/aws-lambda-go/cmd/build-lambda-zip
$ set GOOS=linux
$ set GOARCH=amd64
$ go build -o shorten main.go
$ build-lambda-zip -o deployment.zip shorten

Replace $ROLE with your Role ARN, and deploy both functions.

$ aws lambda create-function --region us-east-1 --function-name ShortenFunction --zip-file fileb://./deployment.zip --runtime go1.x --tracing-config Mode=Active --role $ROLE --handler shorten

If the function already exist, use update-function-code command instead.

$ aws lambda update-function-code --function-name ShortenFunction --region us-east-1 --zip-file fileb://./deployment.zip

Similarly, deploy the redirect function by changing the name from ShortenFunction to RedirectFunction and executable file from shorten to redirect.

Set up API Gateway

Go to API Gateway console. Under APIs click on Create API, choose a name, and create your API.

Under YourAPI/Resources click Actions/Create Resource. Enter "Shorten" as a name and "/shorten" as path. Check Enable API Gateway CORS.

Create a POST method for "/shorten" resource with Actions/Create Method. Select "Lambda Function" for Integration type, check Use Lambda Proxy integration, select "us-east-1" as Lambda Region, select your "ShortenFunction" Lambda function under Lambda Function, then click Save.

Create another resource called "Redirect". Enable Configure as proxy resource and enter "{short_url+}" under Resource path. Create a ANY method, select Lambda Function proxy under Integration type, and select your "RedirectFunction" Lambda function.

To make your URL shortener API public, click Actions/Deploy API. Select "[New Stage]" under Deployment stage, enter "prod" for Stage name, and click Create. The API should now be available on Invoke URL, which can be found at YourAPI/Stages/prod.

Wrapping up

Test your URL shortener API.

$ curl $INVOKE_URL/shorten -X POST -d '{"url":"https://outcrawl.com/getting-started-microservices-go-grpc-kubernetes/"}'
{"short_url":"SHORT_URL"}

Then navigate to INVOKE_URL/SHORT_URL with your browser.

Entire source code is available on GitHub.

Newsletter

Get awesome articles delivered right to your doorstep

Protected by reCAPTCHA - Privacy - Terms

Related