1. DynamoDB Table Creation
Create a pay-per-request NoSQL table in your region and seed the initial view counter record.
aws dynamodb create-table \
--table-name visitor-counter \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region us-west-2
Seed the initial counter record:
aws dynamodb put-item \
--table-name visitor-counter \
--item '{"id": {"S": "site"}, "count": {"N": "0"}}' \
--region us-west-2
2. IAM Execution Role & Access Policies
Create a trust policy and IAM execution role allowing Lambda to output CloudWatch logs and execute atomic updates on DynamoDB.
cat <<EOF > lambda-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "lambda.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
EOF
aws iam create-role \
--role-name visitor-counter-lambda-role \
--assume-role-policy-document file://lambda-trust-policy.json
aws iam attach-role-policy \
--role-name visitor-counter-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
cat <<EOF > lambda-dynamo-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": "arn:aws:dynamodb:*:*:table/visitor-counter"
}
]
}
EOF
aws iam put-role-policy \
--role-name visitor-counter-lambda-role \
--policy-name DynamoDBVisitorCounterAccess \
--policy-document file://lambda-dynamo-policy.json
3. Lambda Function Code
This Python script handles atomic updates on DynamoDB and enforces global CORS headers on all HTTP responses.
cat << 'EOF' > lambda_function.py
import json
import boto3
def lambda_handler(event, context):
cors_headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS'
}
try:
dynamodb = boto3.resource('dynamodb', region_name='us-west-2')
table = dynamodb.Table('visitor-counter')
response = table.update_item(
Key={'id': 'site'},
UpdateExpression='ADD #c :val',
ExpressionAttributeNames={'#c': 'count'},
ExpressionAttributeValues={':val': 1},
ReturnValues='UPDATED_NEW'
)
views = int(response['Attributes']['count'])
return {
'statusCode': 200,
'headers': cors_headers,
'body': json.dumps({'views': views})
}
except Exception as e:
return {
'statusCode': 200,
'headers': cors_headers,
'body': json.dumps({'views': 'Error', 'details': str(e)})
}
EOF
zip function.zip lambda_function.py
aws lambda update-function-code \
--function-name increment-visitor-counter \
--zip-file fileb://function.zip \
--region us-west-2
4. HTTP API Gateway Setup & Routing
Create an HTTP API Gateway, assign global CORS parameters, configure the $default catch-all route, and attach execution permissions.
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
# 1. Create HTTP API
API_ID=$(aws apigatewayv2 create-api \
--name visitor-counter-api \
--protocol-type HTTP \
--target arn:aws:lambda:us-west-2:${ACCOUNT_ID}:function:increment-visitor-counter \
--region us-west-2 \
--query "ApiId" --output text)
# 2. Configure Global CORS
aws apigatewayv2 update-api \
--api-id ${API_ID} \
--cors-configuration "AllowOrigins=*,AllowMethods=GET,POST,OPTIONS,AllowHeaders=*" \
--region us-west-2
# 3. Add $default Route to prevent 404 path drops
INTEGRATION_ID=$(aws apigatewayv2 get-integrations --api-id ${API_ID} --region us-west-2 --query "Items[0].IntegrationId" --output text)
aws apigatewayv2 create-route \
--api-id ${API_ID} \
--route-key '$default' \
--target "integrations/${INTEGRATION_ID}" \
--region us-west-2
# 4. Grant Invoke Permission
aws lambda add-permission \
--function-name increment-visitor-counter \
--statement-id apigateway-invoke-perm \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-west-2:${ACCOUNT_ID}:${API_ID}/*/*" \
--region us-west-2
5. Frontend Integration & URL Masking
Proxy raw API requests through CloudFront to hide raw AWS endpoints under a clean path (/api/), then consume the data on the client side.
CloudFront Behavior Configuration:
- Origin Domain: Your API Gateway domain (e.g.,
oasi55hqkb.execute-api.us-west-2.amazonaws.com) - Path Pattern:
/api/* - Cache Policy: Set to
CachingDisabledto keep counts real-time.
HTML Element:
<p>Total Visitors: <span id="visitor-count">Loading...</span></p>
JavaScript (script.js):
const apiEndpoint = "/api/";
async function updateVisitorCount() {
try {
const response = await fetch(apiEndpoint);
const data = await response.json();
document.getElementById("visitor-count").innerText = data.views;
} catch (error) {
console.error("Error fetching visitor count:", error);
document.getElementById("visitor-count").innerText = "N/A";
}
}
updateVisitorCount();
6. Deployment & CloudFront Cache Invalidation
Force CloudFront to fetch fresh site assets immediately after deploying updates to S3.
aws cloudfront create-invalidation \
--distribution-id d1nrzvrxt6zac1 \
--paths "/*"
7. Pro-Tips & Key Architectural Insights
- CORS & 404 Disconnect: If Chrome reports a CORS failure, check your API routes first. Unmapped paths return 404s without CORS headers, which browsers misinterpret as CORS violations.
- Atomic Increments: Using DynamoDB's
ADD #c :valsyntax prevents race conditions when multiple site visits happen simultaneously. - Region Explicit Messaging: When working across multiple regions, explicitly define
boto3.resource('dynamodb', region_name='...')in Lambda code to avoid silent cross-region lookup failures.