Chunk 2 is complete — and this was by far the most challenging and rewarding part of the challenge so far.
In this post I'm documenting exactly how I built the serverless visitor counter backend : Lambda, DynamoDB, API Gateway , and the bugs I encountered along the way. The debugging section is the most valuable part, so I've kept it honest and detailed.
What is Chunk 2 about?
The goal is to build a serverless API that:
- Stores a visitor count in a database
- Increments it every time someone visits the resume
- Returns the count to the website to display
The AWS services involved:
- DynamoDB — NoSQL database to store the view count
- Lambda — Python function to read and update the count
- API Gateway — exposes Lambda via a public URL
- JavaScript — calls the API from the resume page
Step 1 — Creating the DynamoDB Table
DynamoDB is AWS's managed NoSQL database. For this project it stores one simple record as the visitor count.
- Go to DynamoDB → Create table
- Table name:
cloud-resume-counter - Partition key:
id→ type String - Leave everything else as default → Create table
Once created, add the initial item:
-
id=1(String) -
views=0(Number)
⚠️ Important: Make sure
idis set as String type, not Number. This caused issues later. DynamoDB was creating duplicate items because of a type mismatch between String"1"and Number1.
Step 2 — Creating the Lambda Function
Lambda is a serverless compute service that runs your code without managing any servers.
- Go to Lambda → Create function
- Function name:
cloud-resume-counter - Runtime: Python 3.14
- Leave everything else as default → Create function
Here's the Python code:
import json
import boto3
from decimal import Decimal
dynamodb = boto3.resource('dynamodb', region_name='ap-south-2')
table = dynamodb.Table('cloud-resume-counter')
def lambda_handler(event, context):
# Get current count
response = table.get_item(Key={'id': '1'})
item = response.get('Item', {})
views = item.get('views', 0)
# Increment count
views = int(views) + 1
# Update the count in DynamoDB
table.update_item(
Key={'id': '1'},
UpdateExpression='SET #v = :val',
ExpressionAttributeNames={'#v': 'views'},
ExpressionAttributeValues={':val': Decimal(views)}
)
# Return the new count
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
},
'body': json.dumps({'views': views})
}
Click Deploy to save the function.
Step 3 — IAM Permissions
By default Lambda has no access to DynamoDB. Permissions must be granted explicitly.
- Go to Configuration tab → Permissions
- Click the execution role name
- In IAM → Add permissions → Attach policies
- Search for
AmazonDynamoDBFullAccess→ attach it
Skipping this step results in an AccessDeniedException when the function runs.
Bugs I Encountered Along the Way
This section documents the real issues I ran into because the debugging process is where the actual learning happens.
Bug 1 — AccessDeniedException
Lambda could not access DynamoDB at all.
Fix: attach AmazonDynamoDBFullAccess policy to the Lambda execution role.
Bug 2 — Region Mismatch
Lambda was created in eu-north-1 and DynamoDB in ap-south-2 by mistake. Lambda was looking for the table in the wrong region and returning empty responses.
💡 Key lesson: Lambda and DynamoDB must be in the same AWS region. Always verify the region selector in the top right corner of the AWS Console before creating resources.
Bug 3 — DynamoDB Type Mismatch
The table ended up with two items: one with id as String "1" and another with id as Number 1. Lambda was creating a new item instead of updating the existing one.
Fix: delete the Number 1 item, keep only the String "1" item.
Bug 4 — KeyError: 'Item'
This error persisted because Lambda was connecting to the wrong region and could not find the item at all. Aligning Lambda and DynamoDB to the same region resolved it.
Step 4 — Setting Up API Gateway
API Gateway provides a public HTTPS URL in front of the Lambda function so the browser can call it.
- Go to API Gateway → Create API → HTTP API → Build
- Add integration → Lambda → select
cloud-resume-counter - API name:
cloud-resume-api - Route: GET
/count - Stage:
$defaultwith auto-deploy enabled - Create
The invoke URL will look like:
https://abc123.execute-api.ap-south-2.amazonaws.com/count
Test it in the browser — the response should be:
{"views": 1}
Step 5 — Connecting the Resume Website
The final step — adding JavaScript to index.html to fetch and display the count.
Add this where the counter should appear:
<div class="visitor-counter">
This resume has been viewed
<span id="views-count">...</span> times
</div>
Add this script before </body>:
fetch('https://YOUR-API-GATEWAY-URL/count')
.then(response => response.json())
.then(data => {
document.getElementById('views-count').textContent = data.views;
})
.catch(error => {
console.error('Error fetching views:', error);
document.getElementById('views-count').textContent = '-';
});
Upload the updated index.html to S3 and the resume now displays a live visitor counter.
Final Architecture
Browser → CloudFront → S3 (HTML + CSS)
↓
JavaScript fetch()
↓
API Gateway → Lambda (Python) → DynamoDB
increments count stores views
Key Takeaways
- AWS regions matter — services must be in the same region to communicate properly
- IAM permissions are explicit — no service can access another without being granted permission
-
DynamoDB types are strict — String
"1"and Number1are treated as completely different partition keys - CloudWatch logs are essential — when Lambda fails, logs show exactly what went wrong
- Serverless is cost effective — the entire backend runs at essentially $0 on AWS free tier
What's Next
Next up is the CI/CD pipeline using GitHub Actions automating deployments so changes push to S3 automatically on every commit.
Top comments (0)