AWS Builder Center
Building a Serverless URL Shortener with Amazon API Gateway, AWS Lambda, and DynamoDB

Building a Serverless URL Shortener with Amazon API Gateway, AWS Lambda, and DynamoDB

Learn how to build a fast, scalable serverless URL shortener using Amazon API Gateway (HTTP API), AWS Lambda (Python), and Amazon DynamoDB. This hands-on guide covers NoSQL table creation, IAM least-privilege policies, HTTP 301 redirects, and AWS CLI deployment within the AWS Free Tier.

Information System Student
URL shorteners are a classic system design case study—and a genuinely useful utility for sharing clean links across social media, documentation, or marketing campaigns. However, paying for a monthly SaaS subscription or keeping a traditional relational database running 24/7 just to map short codes to target URLs is overkill.
By combining Amazon DynamoDB, AWS Lambda (Python), and Amazon API Gateway (HTTP API), we can build a globally accessible, single-digit millisecond latency URL shortener that scales effortlessly and runs completely free under the AWS Free Tier.
In this hands-on tutorial, we will provision a DynamoDB table, write a single Python Lambda handler to manage both link creation and HTTP redirection, and expose it via a lightweight HTTP API Gateway.

Architecture Overview

1
2
3
4
5
6
7
8
[Client / Browser]
│
├── POST /shorten ──▶ [API Gateway HTTP API] ──▶ [AWS Lambda] ──▶ PutItem [Amazon DynamoDB]
│
└── GET /{code} ──▶ [API Gateway HTTP API] ──▶ [AWS Lambda] ──▶ GetItem [Amazon DynamoDB]
│
▼
(Returns HTTP 301 Redirect)

1. Provisioning the Amazon DynamoDB Table

We need a simple key-value store where the partition key (short_code) maps directly to the original long_url.
Set your environment variables and create the DynamoDB table using the AWS CLI:
1
2
3
4
5
6
7
8
9
export AWS_REGION="us-east-1"
export TABLE_NAME="UrlShortenerLinks"

aws dynamodb create-table \
--table-name "$TABLE_NAME" \
--attribute-definitions AttributeName=short_code,AttributeType=S \
--key-schema AttributeName=short_code,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region "$AWS_REGION"
Using PAY_PER_REQUEST (On-Demand capacity) ensures you only pay for actual reads and writes, while the AWS Free Tier provides 25 GB of storage and millions of requests at zero cost.

2. Writing the AWS Lambda Handler (Python)

We will use Python 3.12 and the built-in boto3 SDK. Our single Lambda function will handle two routes:
  • POST /shorten: Generates a 6-character short code and stores the target URL in DynamoDB.
  • GET /{code}: Looks up the code in DynamoDB and returns an HTTP 301 Moved Permanently redirect header so browsers jump straight to the destination URL.
Create a new directory and a file named lambda_function.py:
1
2
mkdir serverless-url-shortener
cd serverless-url-shortener
Add the following code to lambda_function.py:
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import json
import os
import string
import random
import boto3

dynamodb = boto3.resource("dynamodb")
table_name = os.environ.get("TABLE_NAME", "UrlShortenerLinks")
table = dynamodb.Table(table_name)

def generate_code(length=6):
chars = string.ascii_letters + string.digits
return "".join(random.choices(chars, k=length))

def lambda_handler(event, context):
route_key = event.get("routeKey", "")

# 1. Handle POST /shorten to create a new short link
if route_key == "POST /shorten":
try:
body = json.loads(event.get("body") or "{}")
long_url = body.get("url")

if not long_url or not long_url.startswith(("http://", "https://")):
return {
"statusCode": 400,
"body": json.dumps({"error": "Please provide a valid http:// or https:// URL"})
}

short_code = body.get("custom_code") or generate_code()

table.put_item(
Item={
"short_code": short_code,
"long_url": long_url
}
)

domain = event.get("requestContext", {}).get("domainName", "localhost")
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"short_code": short_code,
"short_url": f"https://{domain}/{short_code}",
"long_url": long_url
})
}
except Exception as e:
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}

# 2. Handle GET /{code} to redirect to the destination URL
elif route_key == "GET /{code}":
short_code = event.get("pathParameters", {}).get("code")

response = table.get_item(Key={"short_code": short_code})
item = response.get("Item")

if not item:
return {
"statusCode": 404,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "Short URL not found"})
}

return {
"statusCode": 301,
"headers": {
"Location": item["long_url"],
"Cache-Control": "no-cache"
},
"body": ""
}

return {"statusCode": 400, "body": json.dumps({"error": "Unsupported route"})}
Package the Python file into a deployment zip archive:
1
zip function.zip lambda_function.py

3. Configuring IAM Permissions and Deploying Lambda

Let's create an IAM role that follows the principle of least privilege—allowing the Lambda function to only perform GetItem and PutItem on our specific DynamoDB table.
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
51
52
# Create IAM Trust Policy
cat <<EOF> trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
EOF


ROLE_ARN=$(aws iam create-role \
--role-name UrlShortenerLambdaRole \
--assume-role-policy-document file://trust-policy.json \
--query "Role.Arn" \
--output text)

# Attach CloudWatch Logs policy
aws iam attach-role-policy \
--role-name UrlShortenerLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Attach least-privilege DynamoDB access policy
TABLE_ARN=$(aws dynamodb describe-table \
--table-name "$TABLE_NAME" \
--query "Table.Arn" \
--output text \
--region "$AWS_REGION")

cat <<EOF> dynamodb-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem"
],
"Resource": "$TABLE_ARN"
}
]
}
EOF


aws iam put-role-policy \
--role-name UrlShortenerLambdaRole \
--policy-name DynamoDBShortenerAccess \
--policy-document file://dynamodb-policy.json
Wait 10 seconds for IAM propagation, then create the Lambda function:
1
2
3
4
5
6
7
8
aws lambda create-function \
--function-name UrlShortenerFunction \
--runtime python3.12 \
--role "$ROLE_ARN" \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip \
--environment "Variables={TABLE_NAME=$TABLE_NAME}" \
--region "$AWS_REGION"

4. Exposing Endpoints with Amazon API Gateway (HTTP API)

Unlike traditional REST APIs, API Gateway HTTP APIs offer lower latency and cost up to 70% less. You can set up the routes quickly via the AWS Management Console:
  1. Open the Amazon API Gateway console and click Create API.
  2. Choose HTTP API and click Build.
  3. Integrations:
    • Click Add integration and choose Lambda.
    • Select your function (UrlShortenerFunction).
    • Give the API a name: UrlShortenerAPI and click Next.
  4. Configure Routes:
    • Click Add route: Method POST, Resource path /shorten -> Target UrlShortenerFunction.
    • Click Add route: Method GET, Resource path /{code} -> Target UrlShortenerFunction.
  5. Click Next, keep the $default stage with auto-deploy enabled, and click Create.
Copy the Invoke URL displayed on the API details page (e.g., https://abcdef123.execute-api.us-east-1.amazonaws.com).

5. Testing Your Serverless URL Shortener

Set your API Gateway endpoint in your terminal and test creating a short link:
1
2
3
4
5
6
export API_URL="[https://abcdef123.execute-api.us-east-1.amazonaws.com](https://abcdef123.execute-api.us-east-1.amazonaws.com)"

# 1. Create a shortened URL
curl -X POST "$API_URL/shorten" \
-H "Content-Type: application/json" \
-d '{"url": "[https://builder.aws.com](https://builder.aws.com)", "custom_code": "awsbuilder"}'
Expected JSON output:
1
2
3
4
5
{
"short_code": "awsbuilder",
"short_url": "[https://abcdef123.execute-api.us-east-1.amazonaws.com/awsbuilder](https://abcdef123.execute-api.us-east-1.amazonaws.com/awsbuilder)",
"long_url": "[https://builder.aws.com](https://builder.aws.com)"
}
Now test the redirect using curl -i (or open the URL directly in your web browser):
1
curl -i "$API_URL/awsbuilder"
You will see an immediate HTTP/2 301 response with location: https://builder.aws.com!

Resource Cleanup

To clean up all resources when finished testing:
1
2
3
4
5
6
7
8
9
10
# 1. Delete Lambda Function
aws lambda delete-function --function-name UrlShortenerFunction --region "$AWS_REGION"

# 2. Delete DynamoDB Table
aws dynamodb delete-table --table-name "$TABLE_NAME" --region "$AWS_REGION"

# 3. Delete IAM Role & Policies
aws iam delete-role-policy --role-name UrlShortenerLambdaRole --policy-name DynamoDBShortenerAccess
aws iam detach-role-policy --role-name UrlShortenerLambdaRole --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name UrlShortenerLambdaRole
(Don't forget to also delete UrlShortenerAPI from the API Gateway console).

Key Takeaways

  1. Single-Digit Millisecond Lookups: Amazon DynamoDB's key-value lookup on a partition key (short_code) stays consistently fast whether your table stores 100 links or 100 million links.
  2. HTTP API Efficiency: Using API Gateway's payload format v2.0 (routeKey) lets a single Lambda function cleanly route multiple HTTP methods with minimal boilerplate.
  3. Browser-Native Redirection: Returning an HTTP 301 or 302 header offloads the redirect directly to the client browser without proxying heavy page payloads through your Lambda function.
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