
Building a Serverless URLShortener
Sharing long URLs can be a hassle. URL shorteners create unique, shareable links. In this blog, we'll build a serverless URL shortener.
- AWS Lambda to run our URL shortening and redirection logic
- Amazon API Gateway to expose HTTP endpoints for our Lambda functions
- Amazon DynamoDB as a NoSQL database to store the URL mappings
- Terraform for provisioning all the AWS resources
- A client sends a POST request to the API Gateway endpoint with the long URL they want to shorten.
- API Gateway invokes the "create URL" Lambda function.
- The Lambda function generates a unique short ID, stores the mapping (short ID -> long URL) in DynamoDB, and returns the short ID.
- To access the original URL, the client sends a GET request to the API Gateway endpoint with the short ID.
- API Gateway invokes the "get URL" Lambda function.
- The Lambda function looks up the short ID in DynamoDB and returns an HTTP 301 redirect to the original long URL.
- A DynamoDB table to store the URL mappings
- An IAM role and policy to grant Lambda functions access to DynamoDB
- Two Lambda functions: one for creating short URLs, another for retrieving the original URLs
- An API Gateway HTTP API with routes and integrations mapped to the Lambda functions
- It parses the request body to get the original long URL.
- Generates a unique 8-character short ID by hashing the long URL and current timestamp.
- Stores the short ID -> long URL mapping in DynamoDB.
- Returns the short ID in the response.
- Extracts the short ID from the request path parameters.
- Queries DynamoDB for the corresponding long URL.
- If found, returns an HTTP 301 redirect to the long URL.
- If not found, returns a 404 error.
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/very-long-url"}'
{"short_id": "a1b2c3d4"}
.-L
flag in curl follows the HTTP 301 redirect to the long URL.Let us use Amazon Q to help us with the problem statement.
a. tf
b. tf
c. tf
d. tf
e. Lambda
i. Create-url.py
ii. Get-url.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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
provider "aws" {
region = var.aws_region
}
# Create zip files for Lambda functions
data "archive_file" "create_url_lambda" {
type = "zip"
source_file = "${path.module}/lambda/create_url.py"
output_path = "${path.module}/lambda/create_url.zip"
}
data "archive_file" "get_url_lambda" {
type = "zip"
source_file = "${path.module}/lambda/get_url.py"
output_path = "${path.module}/lambda/get_url.zip"
}
# DynamoDB Table
resource "aws_dynamodb_table" "url_table" {
name = "${var.project_name}-urls-${var.environment}"
billing_mode = "PAY_PER_REQUEST"
hash_key = "short_id"
attribute {
name = "short_id"
type = "S"
}
tags = {
Environment = var.environment
Project = var.project_name
}
}
# IAM Role for Lambda
resource "aws_iam_role" "lambda_role" {
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# IAM Policy for Lambda to access DynamoDB
resource "aws_iam_role_policy" "lambda_dynamodb_policy" {
name = "${var.project_name}-lambda-dynamodb-policy-${var.environment}"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = ["dynamodb:PutItem", "dynamodb:GetItem"],
Resource = aws_dynamodb_table.url_table.arn
}
]
})
}
# Lambda basic execution role
resource "aws_iam_role_policy_attachment" "lambda_basic" {
name = "${var.project_name}-lambda-role-${var.environment}"
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.lambda_role.name
}
# Lambda Functions
resource "aws_lambda_function" "create_url" {
filename = data.archive_file.create_url_lambda.output_path
function_name = "${var.project_name}-create-url-${var.environment}"
role = aws_iam_role.lambda_role.arn
handler = "create_url.lambda_handler"
runtime = "python3.9"
environment {
variables = {
DYNAMODB_TABLE = aws_dynamodb_table.url_table.name
}
}
tags = {
Environment = var.environment
Project = var.project_name
}
}
resource "aws_lambda_function" "get_url" {
filename = data.archive_file.get_url_lambda.output_path
function_name = "${var.project_name}-get-url-${var.environment}"
role = aws_iam_role.lambda_role.arn
handler = "get_url.lambda_handler"
runtime = "python3.9"
environment {
variables = {
DYNAMODB_TABLE = aws_dynamodb_table.url_table.name
}
}
tags = {
Environment = var.environment
Project = var.project_name
}
}
# API Gateway
resource "aws_apigatewayv2_api" "url_shortener" {
name = "${var.project_name}-api-${var.environment}"
protocol_type = "HTTP"
cors_configuration {
allow_origins = ["*"]
allow_methods = ["GET", "POST", "OPTIONS"]
allow_headers = ["content-type"]
}
}
resource "aws_apigatewayv2_stage" "url_shortener" {
api_id = aws_apigatewayv2_api.url_shortener.id
name = var.environment
auto_deploy = true
}
# API Gateway Integrations and Routes
resource "aws_apigatewayv2_integration" "create_url" {
api_id = aws_apigatewayv2_api.url_shortener.id
integration_type = "AWS_PROXY"
integration_method = "POST"
integration_uri = aws_lambda_function.create_url.invoke_arn
}
resource "aws_apigatewayv2_integration" "get_url" {
api_id = aws_apigatewayv2_api.url_shortener.id
integration_type = "AWS_PROXY"
integration_method = "POST"
integration_uri = aws_lambda_function.get_url.invoke_arn
}
resource "aws_apigatewayv2_route" "create_url" {
api_id = aws_apigatewayv2_api.url_shortener.id
route_key = "POST /url"
target = "integrations/${aws_apigatewayv2_integration.create_url.id}"
}
resource "aws_apigatewayv2_route" "get_url" {
api_id = aws_apigatewayv2_api.url_shortener.id
route_key = "GET /{shortId}"
target = "integrations/${aws_apigatewayv2_integration.get_url.id}"
}
# Lambda permissions for API Gateway
resource "aws_lambda_permission" "create_url" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.create_url.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.url_shortener.execution_arn}/*/*"
}
resource "aws_lambda_permission" "get_url" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.get_url.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.url_shortener.execution_arn}/*/*"
}
- Imports Required Libraries
- json : Handles JSON response
- os : Fetches environment variables (DynamoDB table name).
- boto3 : AWS SDK for Python to interact with DynamoDB.
- Key : Helps query DynamoDB items. - Initialize DynamoDB Table
- Reads DYNAMODB_TABLE from environment variables.
- Connects to the DynamoDB table. - Lambda Function ( lambda_handler )
- Extracts short_id from the URL path parameters.
- Queries DynamoDB for the corresponding long_url .
- If found, returns an HTTP 301 redirect to long_url .
- If not found, returns HTTP 404.
- If an error occurs, returns HTTP 500 with the error message.
1
2
3
terraform init
terraform plan
terraform apply
1
2
3
curl -X POST <api_endpoint>/url \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
{ "short_id": "a1b2c3d4" }
1
curl -L <api_endpoint>/a1b2c3d4
Some important notes:-
https://abc123def.execute-api.useast-1.amazonaws.com/prod
Make sure to keep the API endpoint URL for future use
The shortened URLs are stored in DynamoDB and will persist until deleted.
Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.