Serverless Project — Build a Complete Serverless Application
In this tutorial, you will learn about Serverless Project. We cover key concepts, practical examples, and best practices to help you master this topic.
This capstone project guides you through building a production-ready serverless URL shortener with AWS Lambda, API Gateway, DynamoDB, and the Serverless Framework, covering the full development lifecycle.
What You'll Learn
By the end of this project you will have built a complete serverless application: defining infrastructure as code, implementing business logic, handling errors, monitoring with CloudWatch, and setting up CI/CD.
Why It Matters
Building a complete project ties together all the serverless concepts you have learned. This URL shortener demonstrates real-world patterns: REST API design, database access, event-driven processing, monitoring, and deployment.
Real-World Use
URL shorteners like bit.ly need to handle millions of redirects per day with high availability. A serverless implementation scales automatically, costs nothing when idle, and provides single-digit millisecond redirect latency.
flowchart TD
C[Client] -->|POST /shorten| AG[API Gateway]
C -->|GET /{code}| AG
AG -->|Lambda| S[Shorten Function]
AG -->|Lambda| R[Redirect Function]
S --> D[DynamoDB - URLs Table]
R --> D
R -->|301 Redirect| C
D -->|Streams| A[Analytics Lambda]
A --> AN[DynamoDB - Analytics Table]
style AG fill:#f90,color:#fff
Project Structure
url-shortener/
serverless.yml # Infrastructure as code
handlers/
shorten.py # POST /shorten
redirect.py # GET /{code}
analytics.py # DynamoDB Streams trigger
tests/
test_shorten.py # Unit tests
test_redirect.py
.github/
workflows/
deploy.yml # CI/CD pipeline
package.json # Node.js dependencies
requirements.txt # Python dependencies
serverless.yml
service: url-shortener
provider:
name: aws
runtime: python3.9
region: us-east-1
stage: ${opt:stage, 'dev'}
environment:
URLS_TABLE: ${self:service}-urls-${self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:UpdateItem
Resource: !GetAtt UrlsTable.Arn
functions:
shorten:
handler: handlers/shorten.handler
events:
- httpApi:
method: POST
path: /shorten
redirect:
handler: handlers/redirect.handler
events:
- httpApi:
method: GET
path: /{code}
analytics:
handler: handlers/analytics.handler
events:
- stream:
type: dynamodb
arn: !GetAtt UrlsTable.StreamArn
resources:
Resources:
UrlsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-urls-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: code
AttributeType: S
KeySchema:
- AttributeName: code
KeyType: HASH
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Handler Implementation
# handlers/shorten.py
import json
import hashlib
import time
def handler(event, context):
body = json.loads(event.get("body", "{}"))
long_url = body.get("url")
if not long_url:
return {"statusCode": 400, "body": json.dumps({"error": "Missing url"})}
code = hashlib.md5(long_url.encode()).hexdigest()[:8]
print(f"Storing code={code} -> url={long_url}")
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"code": code,
"short_url": f"https://short.ly/{code}",
"long_url": long_url
})
}
# handlers/redirect.py
import json
store = {
"abc123": "https://example.com/very/long/url"
}
def handler(event, context):
code = event.get("pathParameters", {}).get("code")
long_url = store.get(code)
if not long_url:
return {"statusCode": 404, "body": json.dumps({"error": "URL not found"})}
print(f"Redirect {code} -> {long_url}")
return {
"statusCode": 301,
"headers": {"Location": long_url},
"body": ""
}
Testing
# tests/test_shorten.py
import json
import sys
sys.path.insert(0, ".")
from handlers.shorten import handler as shorten_handler
from handlers.redirect import handler as redirect_handler
def test_shorten_url():
event = {"body": json.dumps({"url": "https://example.com"})}
result = shorten_handler(event, None)
assert result["statusCode"] == 201
body = json.loads(result["body"])
assert "code" in body
assert body["long_url"] == "https://example.com"
print(f"PASS: shorten returns 201 with code")
def test_missing_url():
result = shorten_handler({"body": json.dumps({})}, None)
assert result["statusCode"] == 400
print("PASS: missing url returns 400")
def test_redirect():
result = redirect_handler({"pathParameters": {"code": "abc123"}}, None)
assert result["statusCode"] == 301
assert result["headers"]["Location"] == "https://example.com/very/long/url"
print("PASS: redirect returns 301 with Location header")
def test_redirect_not_found():
result = redirect_handler({"pathParameters": {"code": "nonexistent"}}, None)
assert result["statusCode"] == 404
print("PASS: unknown code returns 404")
test_shorten_url()
test_missing_url()
test_redirect()
test_redirect_not_found()
print("\nAll tests passed!")
Common Mistakes
Not handling DynamoDB Streams ordering: Stream records within a shard are ordered. Process them sequentially to maintain consistency.
Forgetting to configure DynamoDB Streams: Streams must be enabled on the table with the appropriate view type.
Not setting up CloudWatch alarms: Without alarms on error rates and throttles, issues go undetected until users report them.
Skipping local testing: Test functions locally with mock events before deploying to avoid costly deployment-test cycles.
Not implementing idempotency: Duplicate URL submissions should return the same code. Use conditional writes in DynamoDB.
Practice Questions
What is the architecture of the URL shortener? API Gateway receives requests, Lambda functions handle shortening and redirecting, DynamoDB stores mappings, and Streams trigger analytics.
How does the redirect function achieve low latency? It does a simple DynamoDB GetItem by primary key, which has single-digit millisecond latency, and returns a 301 redirect.
What would you add to make this production-ready? Custom domain, HTTPS, CloudFront CDN, analytics dashboard, Rate Limiting, and user authentication.
How does the DynamoDB Streams analytics function work? It receives item changes and updates a separate analytics table with click counts and timestamps.
Challenge: Extend the URL shortener with user authentication, click analytics dashboard, custom short codes, and QR Code Generation.
FAQ
Mini Project
You have already built the URL shortener as the capstone project. Extend it with analytics tracking that records each redirect with timestamp and user agent.
import json
import time
analytics_store = []
def track_redirect(code, event):
record = {
"code": code,
"timestamp": time.time(),
"user_agent": event.get("headers", {}).get("user-agent", "unknown"),
"ip": event.get("requestContext", {}).get("identity", {}).get("sourceIp", "unknown")
}
analytics_store.append(record)
print(f"Analytics: {code} accessed from {record['ip']}")
def handler(event, context):
code = event.get("pathParameters", {}).get("code")
track_redirect(code, event)
return {"statusCode": 301, "headers": {"Location": f"https://example.com/{code}"}, "body": ""}
result = handler({"pathParameters": {"code": "abc"}, "headers": {"user-agent": "Mozilla/5.0"}, "requestContext": {"identity": {"sourceIp": "1.2.3.4"}}}, None)
print(f"Redirect: {result['statusCode']}")
print(f"Analytics stored: {len(analytics_store)} events")
What's Next
Congratulations on completing the Serverless module. Continue to Stripe Payments for payment processing integration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro