“Serverless URL Shrinker: From Long to Lightning Fast”

Serverless – The Cloud’s Superpower

Think of Serverless like your favorite food delivery app — say Swiggy, Zomato, or Uber Eats.

You’re hungry. You open the app, place an order, and boom — food gets delivered. You don’t own the restaurant. You don’t worry about the kitchen, the chef, or the delivery guy’s salary. You only care about getting what you need, when you need it.

That’s Serverless computing in the cloud.

You don’t manage servers. You don’t provision or maintain infrastructure. You just trigger a function, get results, and move on — all while the cloud provider handles the heavy lifting in the background.

In this blog, we explore how to harness that power to build something practical, fast, and surprisingly elegant — a URL Shortener, without managing a single server.

Let’s dive into how I crafted this Serverless POC using AWS and a bit of creative hacking with GitHub Pages.

🧩 Key Components of This POC:

1️⃣ Amazon API Gateway

This acts as the front door for our URL shortener. When a user submits a long URL, API Gateway accepts the request and passes it to our backend system. In a production-grade system, we would ideally use a custom domain name (e.g., short.ly/xyz). But to keep things simple here, I’ve used GitHub Pages as a pseudo-custom frontend.

2️⃣ AWS Lambda – The Lightning Bolt

This is where the real magic happens. Lambda handles:

  • Receiving the long URL from API Gateway.

  • Fetching and incrementing a counter in DynamoDB.

  • Encoding the counter using Base62 to generate a unique short key.

  • Creating a zero-byte object in S3 with the WebsiteRedirectLocation set to the original URL.

Like a flash of lightning, it executes and disappears—no servers, no worries.

3️⃣ Amazon DynamoDB – The Smart Notepad

Every time a new URL is shortened, we increment a counter in DynamoDB. That number becomes the unique ID for that URL, which we then Base62 encode to keep it short and URL-friendly. Think of DynamoDB as the system's brain—quietly remembering how many URLs you've shortened.

4️⃣ Amazon S3 – The Redirection Engine

Here’s the clever trick. Once we generate the short key, Lambda creates an empty object in S3 and sets the WebsiteRedirectLocation to the original long URL. When a user accesses that short URL, S3 takes over and does the redirection. No code runs. No compute cost. Just a clean redirect, every single time.

5️⃣ GitHub Pages – The Friendly Face

To give users a way to enter their long URLs, I created a simple static HTML page hosted on GitHub Pages. It’s lightweight, fast, and integrates beautifully with API Gateway. It acts like the receptionist in our system—taking the user’s input and forwarding it to the serverless backend.

Next up Roll up your sleeves, I’ll walk you through the code, explain the flow, and show you how you can build one yourself — or even enhance it with analytics, authentication, or expiration logic!

Step 1: Create your S3 bucket enable webhosting,Disable block from public access update the Bucket policy to allow public access

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::funbucket-1-06-2025/*"
        }
    ]
}

Step 2: Create a Counter Table in Dynamo DB . The countervalue in the DynamoDB is the reason unique id you could see

Step 3 : Create a IAM Role for Lamda that have necessary access to perform actions on our S3 bucket and Dynamo DB table

for simplicity s3 full access and DynamoDB full access Usually as a good security practice ,you should allow only access to paricular resource alone

Step 4: Let’s build our Lambda, which actually does all the heavy Lifting🏋️

I will attach the code Logic here 😎🪄

import boto3
import re
import json

print('Loading function...')

s3 = boto3.client('s3', region_name='us-east-1')
dynamodb = boto3.client('dynamodb', region_name='us-east-1')

S3_BUCKET = "funbucket-1-06-2025"
S3_KEY_PREFIX = ""
SHORT_DOMAIN = "https://koushalakash26.github.io/URLShortner/"
DYNAMO_TABLE = "counters"
APP_NAME = "url_shortner"

def add_http(url):
    if not re.match(r'^(f|ht)tps?://', url, re.IGNORECASE):
        url = "http://" + url
    return url

def get_short_file_name(counter_value):
    base62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    short_file_name = ""
    base = len(base62)
    counter_value = int(counter_value)

    while counter_value > 0:
        counter_value, remainder = divmod(counter_value, base)
        short_file_name = base62[remainder] + short_file_name

    return short_file_name

def lambda_handler(event, context):
    headers = {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",  # Allow CORS from all origins
        "Access-Control-Allow-Methods": "POST, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type"
    }

    try:
        # Handle both cases: event['body'] JSON string or event itself is the body dict
        if 'body' in event and event['body']:
            if isinstance(event['body'], str):
                body = json.loads(event['body'])
            else:
                body = event['body']
        else:
            body = event

        url = body.get('url') if isinstance(body, dict) else None

        if not url:
            return {
                'statusCode': 400,
                'headers': headers,
                'body': json.dumps({'error': "Missing 'url' in request body"})
            }

        # Increment counter atomically in DynamoDB
        response = dynamodb.update_item(
            TableName=DYNAMO_TABLE,
            Key={'application': {'S': APP_NAME}},
            UpdateExpression='ADD counterValue :increment',
            ExpressionAttributeValues={':increment': {'N': '1'}},
            ReturnValues='UPDATED_NEW'
        )

        counter_value = response['Attributes']['counterValue']['N']
        short_key = get_short_file_name(counter_value)

        s3.put_object(
            Bucket=S3_BUCKET,
            Key=S3_KEY_PREFIX + short_key,
            Body='',
            ContentType='text/html',
            WebsiteRedirectLocation=add_http(url)
        )

        return {
            'statusCode': 200,
            'headers': headers,
            'body': json.dumps({'shortUrl': SHORT_DOMAIN + short_key})
        }

    except Exception as e:
        print(f"Error: {e}")
        return {
            'statusCode': 500,
            'headers': headers,
            'body': json.dumps({'error': str(e)})
        }

In the above code snippet you could see short_domain variable as my hosted repo by github pages feature.

🧠 How It Works:

  1. GitHub Pages automatically serves 404.html for any non-existent paths

  2. This script runs on that 404 page

  3. It extracts the short key from the URL

  4. Redirects to your S3 static site with that key appended

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Redirecting...</title>
  <script>
    // S3 static website base URL
    const baseURL = "http://funbucket-1-06-2025.s3-website-us-east-1.amazonaws.com/";

    // Get the full path after /URLShortner/, e.g., "ab" in "/URLShortner/ab"
    const path = window.location.pathname.replace(/^\/+|\/+$/g, ''); // full path like "URLShortner/ab"

    const redirectKey = path.replace(/^URLShortner\//, ''); // strip "URLShortner/" prefix

    if (redirectKey) {
      window.location.href = baseURL + redirectKey;
    } else {
      document.write("<h1>Not a valid short URL</h1>");
    }
  </script>
</head>
<body>
  <p>Redirecting...</p>
</body>
</html>

Step 5: Now let's build our API Gateway it’s a connector 🏗️ between the Frontend and Backend

enable proxy Integeration ,Create a Stage and Deploy the API

Step:6 Create a simple Frontend where the user can give the long url

I will provide the code snippet ,it is very simple one you are welcome to add your creativity when you try

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>URL Shortener</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        text-align: center;
        margin-top: 50px;
      }
      input {
        padding: 10px;
        width: 300px;
        font-size: 16px;
      }
      button {
        padding: 10px 20px;
        font-size: 16px;
        margin-left: 10px;
      }
      #result {
        margin-top: 20px;
        font-size: 18px;
        color: green;
      }
    </style>
  </head>
  <body>
    <h1>URL Shortener</h1>
    <form id="urlForm">
      <input type="text" id="urlInput" placeholder="Enter a URL" required />
      <button type="submit">Shorten</button>
    </form>
    <p id="result"></p>

    <script>
      const apiUrl = "https://cxw6qkn65j.execute-api.us-east-1.amazonaws.com/prod"; // Replace with your API Gateway invoke URL

      document.getElementById("urlForm").addEventListener("submit", async function (e) {
        e.preventDefault();
        const url = document.getElementById("urlInput").value;
        const resultElement = document.getElementById("result");
        resultElement.textContent = "Generating short URL...";

        try {
          const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ url: url }),
          });

          const data = await response.json();
          console.log("Response from API:", data);

          // Use data directly since API response is already parsed JSON
          const payload = data;

          if (response.ok && payload.shortUrl) {
            resultElement.innerHTML = `<strong>Short URL:</strong> <a href="${payload.shortUrl}" target="_blank">${payload.shortUrl}</a>`;
          } else {
            console.error("Invalid response structure:", payload);
            resultElement.textContent = "Error: Invalid response from server.";
          }
        } catch (error) {
          console.error("Fetch error:", error);
          resultElement.textContent = "Error: " + error.message;
        }
      });
    </script>
  </body>
</html>

I created this as index.html and published in github pages

Now let’s try with some long url and check 🤞(Remeber i use github pages as alternative for custom domain which will be a right way to do in production env)

https://en.wikipedia.org/wiki/List_of_cognitive_biases_and_heuristics#Choice-supportive_bias

Wola!🪄 It works perfectly fine! ✨ Kudos ✌️ 😎 for Coming up to here That’s it for this Blog. Stay Curious and Happy Building 💪⚡

0
Subscribe to my newsletter

Read articles from Koushal Akash RM directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Koushal Akash RM
Koushal Akash RM