Building a Serverless Visitor Counter with Lambda and DynamoDB

Part 7 of the AWS Cloud Resume Challenge.

Frontend Website Code: https://github.com/Saifulrubkhan/Saiful_CloudResumeWebsite
Backend Serverless Code (Lambda + DynamoDB): https://github.com/Saifulrubkhan/cloud-resume-backend

With the frontend of the website fully secured and cost monitoring configured, the next step was adding the first dynamic feature to the project: a visitor counter.

Backend source for this feature lives in Cloud-Resume-Backend. The live HTTP endpoint is:

GET https://lqwo3tc3q5.execute-api.us-east-1.amazonaws.com/prod/api/visitor

The site calls that API from src/js/visitor-counter.js and renders the returned views value.

This feature is a core component of the AWS Cloud Resume Challenge because it demonstrates how data flows through a modern cloud architecture. A request begins in the user's browser, triggers backend logic, updates a database, and returns a result to the frontend.

To build this feature, I implemented a serverless architecture using the following AWS services:>
  • Amazon DynamoDB to store the visitor count
  • AWS Lambda to process the request and update the database
This approach removes the need to manage servers while still allowing the application to scale automatically.

Architecture Overview

The visitor counter uses a simple serverless workflow:

flowchart TD
  browser[User Browser] --> apigw[API Gateway]
  apigw --> lambda[AWS Lambda]
  lambda --> ddb[(DynamoDB)]
  ddb --> lambda
  lambda --> apigw
  apigw --> browser
						

Each new browser session triggers a request that increments the visitor count stored in DynamoDB (page navigations within the same session do not).

Step 1 - Setting Up the Database (DynamoDB)

The first step was creating a place to store the visitor count. For this I used Amazon DynamoDB, AWS's fully managed NoSQL database service.

DynamoDB is designed for high availability and low latency, making it well suited for lightweight application data such as a visitor counter. I created a table called: cloud-resume-stats The table was configured with the following key:
  • Partition Key: id (String)
This key uniquely identifies each item stored in the table. To initialize the counter, I manually created the first item in the table with the following attributes:
  • id: visitors
  • count: 0
This item provides the starting value that the Lambda function increments each time the counter is triggered.

Step 2 - Writing the Backend Logic (AWS Lambda)

Next I created an AWS Lambda function responsible for updating the visitor count.

Lambda allows code to run in response to events without provisioning or managing servers. The function executes only when triggered, making it efficient and cost effective for small workloads like this. The function was written in Python using the boto3 library, which is the AWS SDK for Python. The Lambda function performs three main tasks:
  • Connect to the DynamoDB service
  • Increment the visitor count stored in the table
  • Return the updated value to the caller
The most important part of the function is the atomic update operation. Instead of reading the value and writing it back manually, the function uses DynamoDB's update_item operation with an update expression. This allows DynamoDB to handle the increment operation safely.

Why this matters: if multiple users load the website at the same time, DynamoDB ensures the counter increments correctly without race conditions or lost updates.

Step 3 - Securing the Connection with IAM

By default, a Lambda function cannot access other AWS services. Permissions must be explicitly granted through an IAM role.

Each Lambda function runs with an execution role that defines what resources the function can interact with. To follow the Principle of Least Privilege, I created a custom IAM policy granting only the permissions required for the visitor counter.
The policy allows the following actions:
  • dynamodb:GetItem
  • dynamodb:UpdateItem
These permissions are scoped specifically to the ARN of the DynamoDB table.
After attaching the policy to the Lambda execution role, I tested the function using the Lambda test feature to confirm it could successfully update the database.

Step 4 - Creating an Endpoint for the Frontend

In order for the website to trigger the Lambda function, the browser needs a public HTTP endpoint. Instead of exposing the Lambda function directly, I created an endpoint using Amazon API Gateway. API Gateway acts as the entry point for HTTP requests and forwards those requests to the Lambda function.

Using an HTTP API in API Gateway provides a clean and scalable way for frontend applications to communicate with serverless backend services. When a visitor loads the website, the browser sends a request to the API Gateway endpoint. API Gateway then triggers the Lambda function, which updates the visitor counter stored in DynamoDB and returns the updated value to the browser.

Because the website is hosted on a different domain than the API endpoint, I configured CORS (Cross Origin Resource Sharing). CORS allows browsers to safely make requests between different domains. To restrict access and follow security best practices, the API endpoint was configured with the following settings:

  • Allowed Method: GET
  • Allowed Origin: https://khansaiful.com
These restrictions ensure that only my website can send requests to the API endpoint that triggers the visitor counter.

Step 5 - Integrating the Counter with the Frontend

With the backend complete, the final step was connecting the visitor counter to the website. I added a small JavaScript script that runs when the page loads.
The script performs three actions:

  • Sends a GET request to the API Gateway /prod/api/visitor endpoint
  • Waits for the JSON response containing the updated visitor count
  • Updates the visitor counter displayed on the page
When a user visits the site, the following process occurs:
  1. The browser sends a request to API Gateway
  2. Lambda increments the value stored in DynamoDB
  3. The updated visitor count is returned to the browser
  4. The page displays the updated value
This completes the full data flow from frontend to backend and back again.

Result

The website now includes a fully functional serverless visitor counter.

The final architecture looks like this:

flowchart TD
  browser[User Browser] --> apigw[API Gateway]
  apigw --> lambda[AWS Lambda]
  lambda --> ddb[(DynamoDB)]
  ddb --> lambda
  lambda --> apigw
  apigw --> ui[Updated visitor count on page]
						

Each time a new browser session loads the site, the counter increments automatically.

What Is Next

Up to this point most of the infrastructure for this project was created manually through the AWS console. This approach is often referred to as ClickOps.

While this method works well for learning, production environments rely on Infrastructure as Code to ensure infrastructure is reproducible, version controlled, and easier to maintain.

In the next stage of the project I am converting this infrastructure into Terraform. That work is in progress: the visitor counter already runs via API Gateway, Lambda, and DynamoDB, and Terraform will take over provisioning those resources as code.


Next Up: Infrastructure as Code - Rebuilding the Project with Terraform