AWS Builder Center

Build a Serverless URL Shortener on AWS with Lambda, API Gateway and DynamoDB

Learn how to build a serverless URL shortener on AWS in about an hour. This beginner-friendly, step-by-step guide uses Lambda, API Gateway and DynamoDB, with least-privilege IAM permissions and no servers to manage. One-line version (if there’s a character limit): Build a serverless URL shortener with AWS Lambda, API Gateway and DynamoDB, step by step.

B-tech CSE Student | Aspiring Cloud Security Engineer | AWS & Cloud Computing | Cyber security

Introduction

When I started learning AWS, I kept reading about “serverless” without knowing what it looked like in practice. The best way to learn was to build something small but real. In this article I’ll walk through a URL shortener: you send it a long link, it gives you a short code, and visiting the short link redirects you to the original.
It’s a good first project because it touches three core AWS services and teaches ideas you’ll reuse everywhere: event-driven code, NoSQL data modeling, and least-privilege permissions.

Architecture

The flow has three parts:
  1. Amazon API Gateway receives HTTP requests.
  2. AWS Lambda runs the logic to create or look up a link.
  3. Amazon DynamoDB stores the mapping between short code and original URL.
There are two routes:
  • POST /links takes a long URL and returns a short code
  • GET /{code} looks up the code and redirects to the original URL

Step 1: Create the DynamoDB table

  1. Open the DynamoDB console and choose Create table.
  2. Name it short-links.
  3. Set the partition key to code (type: String).
  4. Choose On-demand capacity so you only pay for what you use.
Each item will look like { "code": "aB3xY9", "url": "https://example.com/some/very/long/path" }.

Step 2: Write the Lambda function

Create a Lambda function named url-shortener using the Python 3.12 runtime (or the latest Python runtime available). Under Configuration → Environment variables, add TABLE_NAME = short-links. Then paste in this code:
python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import json
import os
import random
import string

import boto3
from botocore.exceptions import ClientError

table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"])

def response(status, body):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body),
}

def handler(event, context):
method = event["requestContext"]["http"]["method"]

# Create a short link
if method == "POST":
body = json.loads(event.get("body") or "{}")
url = body.get("url", "")

if not url.startswith(("http://", "https://")):
return response(400, {"error": "Please provide a valid http(s) URL"})

code = "".join(random.choices(string.ascii_letters + string.digits, k=6))

try:
table.put_item(
Item={"code": code, "url": url},
ConditionExpression="attribute_not_exists(code)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return response(500, {"error": "Code collision, please retry"})
raise

return response(201, {"code": code})

# Redirect from a short link
code = event.get("pathParameters", {}).get("code")
item = table.get_item(Key={"code": code}).get("Item")

if not item:
return response(404, {"error": "Link not found"})

return {"statusCode": 302, "headers": {"Location": item["url"]}}
A few things worth noticing:
  • The ConditionExpression stops a new link from overwriting an existing code, which is a small but important safety check.
  • Validating that the URL starts with http:// or https:// prevents junk and unsafe schemes like javascript: from being stored.

Step 3: Give Lambda permission to use DynamoDB

By default, your function can’t touch DynamoDB. Open the function’s execution role in IAM and attach an inline policy that grants only what’s needed:
json
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/short-links"
}
]
}
Replace REGION and ACCOUNT_ID with your own values. This is the principle of least privilege: the function can only read and write one table, nothing more.

Step 4: Expose it with API Gateway

  1. In API Gateway, create an HTTP API.
  2. Add a Lambda integration pointing to url-shortener.
  3. Create two routes: POST /links and GET /{code}.
  4. Deploy with the default stage and copy the Invoke URL.

Step 5: Test it

Create a short link:
bash
1
2
3
curl -X POST https://YOUR_API_ID.execute-api.REGION.amazonaws.com/links \
-H "Content-Type: application/json" \
-d '{"url": "https://aws.amazon.com/builders/"}'
You’ll get back something like {"code": "aB3xY9"}. Open https://YOUR_API_ID.execute-api.REGION.amazonaws.com/aB3xY9 in your browser, and you should land on the original page.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article