DEV Community

Priyanka Vuddemari
Priyanka Vuddemari

Posted on

Cloud Resume Challenge Week 2 — Building the Serverless Backend with Lambda, DynamoDB & API Gateway

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.

  1. Go to DynamoDB → Create table
  2. Table name: cloud-resume-counter
  3. Partition key: id → type String
  4. Leave everything else as default → Create table

Once created, add the initial item:

  • id = 1 (String)
  • views = 0 (Number)

⚠️ Important: Make sure id is set as String type, not Number. This caused issues later. DynamoDB was creating duplicate items because of a type mismatch between String "1" and Number 1.


Step 2 — Creating the Lambda Function

Lambda is a serverless compute service that runs your code without managing any servers.

  1. Go to Lambda → Create function
  2. Function name: cloud-resume-counter
  3. Runtime: Python 3.14
  4. 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})
    }
Enter fullscreen mode Exit fullscreen mode

Click Deploy to save the function.


Step 3 — IAM Permissions

By default Lambda has no access to DynamoDB. Permissions must be granted explicitly.

  1. Go to Configuration tab → Permissions
  2. Click the execution role name
  3. In IAM → Add permissionsAttach policies
  4. 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.

  1. Go to API Gateway → Create APIHTTP APIBuild
  2. Add integration → Lambda → select cloud-resume-counter
  3. API name: cloud-resume-api
  4. Route: GET /count
  5. Stage: $default with auto-deploy enabled
  6. Create

The invoke URL will look like:

https://abc123.execute-api.ap-south-2.amazonaws.com/count
Enter fullscreen mode Exit fullscreen mode

Test it in the browser — the response should be:

{"views": 1}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 = '-';
  });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 Number 1 are 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)