
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.
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 anHTTP 301 Moved Permanentlyredirect 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-shortenerAdd 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.py3. 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.jsonWait 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:
- Open the Amazon API Gateway console and click Create API.
- Choose HTTP API and click Build.
- Integrations:
- Click Add integration and choose Lambda.
- Select your function (
UrlShortenerFunction). - Give the API a name:
UrlShortenerAPIand click Next.
- Configure Routes:
- Click Add route: Method
POST, Resource path/shorten-> TargetUrlShortenerFunction. - Click Add route: Method
GET, Resource path/{code}-> TargetUrlShortenerFunction.
- Click Add route: Method
- Click Next, keep the
$defaultstage 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
- 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. - 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. - Browser-Native Redirection: Returning an
HTTP 301or302header offloads the redirect directly to the client browser without proxying heavy page payloads through your Lambda function.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article