Design and Build a URL Shortener on AWS

Build a serverless URL shortener using API Gateway, Lambda, and DynamoDB.

Introduction

30 Second Summary

Every time you paste a long, messy URL into a tool like Bit.ly, a system somewhere stores your link, generates a short code, and remembers the connection for anyone who clicks it later.

In this project, you will build your own URL shortener service on AWS. You'll send it a long URL, get back a short code, and when anyone visits that code, they get redirected to the original page.

What You'll Build

A fully functional URL shortener API running on AWS serverless infrastructure that you can call from your terminal.

By the end of this project, you'll have:

  • A DynamoDB table storing your shortened URL mappings.
  • A Lambda function containing the shortening and redirect logic.
  • A live API Gateway endpoint exposing your service to the internet, testable with curl from your terminal.
  • Secret Mission: Add a visit counter that tracks how many times each short URL is accessed.

Are there any prerequisites?

You need an AWS account to complete this project. If you don't have one yet, you can sign up at the AWS Free Tier page.

Understand the Architecture and Sign Into AWS

Before writing a single line of code, you need to map out the components your system needs. Every system design starts with one question: what are the building blocks, and how do they connect?

In this step, you will map out the three-layer architecture behind your URL shortener, sign into AWS, and confirm you can access the services you will use to build it.

In this step, get ready to:
  • Understand the three-layer architecture (API, compute, database).
  • Sign into the AWS Management Console and set your region.
  • Verify access to DynamoDB, Lambda, and API Gateway.
Understand the three-layer architecture

Your URL shortener has three layers. This is the same pattern used by virtually every web application you interact with daily.

  • API Gateway receives HTTP requests from the internet.
  • Lambda runs your Python code to process those requests.
  • DynamoDB stores the URL mappings.

Why separate into layers?

Separating concerns into layers means each layer can scale independently. If your URL shortener goes viral and gets thousands of requests per second, AWS can spin up more Lambda copies without touching the database configuration.

This is the same architectural pattern behind Instagram, Uber, and Netflix. The names of the services change, but the three layers (API, compute, database) stay the same.

Sign into the AWS Management Console
  • Open your browser and navigate to https://console.aws.amazon.com/.
  • Sign in with your AWS account credentials.
  • In the upper-right corner of the console, click the Region selector (it displays the name of the currently selected region).
  • Choose US East (N. Virginia) from the dropdown list. This sets your region to us-east-1.

Why us-east-1?

US East (N. Virginia) is the most commonly used AWS region. It guarantees all free tier services are available and is the default for most AWS documentation and tutorials.

Can't find the Region selector?

Look in the upper-right corner of the page, next to your account name. It shows the currently selected region name (like "Ohio" or "Oregon"). Click it to open the region dropdown.

help me find the Region selector in the AWS Management Console

Verify access to DynamoDB, Lambda, and API Gateway

You do not need to create anything yet. This confirms your account has the necessary access to the three services you will use throughout this project.

  • Click the search bar at the top of the console.
  • Type DynamoDB and press Enter to open the DynamoDB console.
  • Confirm the DynamoDB dashboard loads without errors.
  • Click the search bar again, type Lambda, and press Enter to open the Lambda console.
  • Confirm the Lambda dashboard loads without errors.
  • Click the search bar one more time, type API Gateway, and press Enter to open the API Gateway console.
  • Confirm the API Gateway dashboard loads without errors.

What should I see?

Each service page should load a dashboard or welcome screen. You might see empty tables or a "Get started" prompt. That is expected. The important thing is that the page loads without an access denied error.

Seeing an access denied error?

Make sure you are signed in as the root user or an IAM user with administrator access. If you are using a restricted IAM user, you may not have permissions for all three services.

help me fix an access denied error in the AWS Console

Your environment is ready. Next up, you will design your database and make your first real system design decision: choosing the right type of database for the job.

Design and Create Your Database

You've mapped out the three-layer architecture and confirmed your AWS access. Now it's time to build the first layer of your URL shortener: the database.

Your URL shortener needs to store mappings between short codes and long URLs. Each lookup follows a simple pattern: given a short code, return the original URL. This is a pure key-value lookup with no complex relationships between records.

Why DynamoDB instead of a relational database?

A relational database (SQL) is designed for complex queries that join multiple tables together. Your URL shortener only ever asks one question: "given this short code, what is the long URL?" There are no joins. No relationships. Just a key and a value.

DynamoDB is a NoSQL key-value database that delivers single-digit millisecond reads at any scale. Its free tier gives you 25 GB of storage, 25 write capacity units, and 25 read capacity units per month.

In this step, get ready to:
  • Create a DynamoDB table to store URL mappings.
  • Verify the table is active and ready for data.
  • Understand the partition key design trade-off.
Create your DynamoDB table
  • In the AWS Console, type DynamoDB in the top search bar and select it to open the DynamoDB console.
  • In the left navigation pane, choose Tables.
  • Choose Create table.
  • For Table name, enter url-shortener-links.
  • For Partition key, enter short_code and leave the type as String.
  • For Table settings, keep the default selection of Default settings. This uses provisioned mode, which is covered by the free tier.
  • Choose Create table.

What is a partition key?

A partition key is how DynamoDB finds your data. Think of it like the index in the back of a textbook. Instead of reading every page, you jump straight to the entry you need.

You chose short_code as your partition key because that is exactly how your system will look up data. When someone visits a short URL, DynamoDB uses the short code to instantly locate the matching long URL.

Verify your table is active
  • Wait for the table status to change from Creating to Active. This takes about 10-30 seconds.
  • Once active, click on your table name url-shortener-links to explore it.
  • Click Explore table items. Notice it shows 0 items returned.

The table is empty and ready to store URL mappings. Each item you store will have a short_code (the partition key) and a long_url (the destination URL).

Table stuck on Creating?

If your table status hasn't changed to Active after 60 seconds, refresh the page. If it still shows Creating, check that your region is set to us-east-1 in the top-right corner of the console.

Help me troubleshoot my DynamoDB table creation

Understand the partition key trade-off

You just made an important system design decision. Take a moment to think about why it matters.

Why does the key choice matter?

You chose short_code as your partition key because that is how your system will look up data. Every request to your URL shortener says: "given this short code, find the URL." DynamoDB can answer that question in milliseconds because the partition key is exactly what it uses to locate records.

If you had chosen long_url as the key instead, looking up a record by short code would require scanning every item in the table. In system design, choosing the right key is one of the most important decisions you make. The key determines how fast your database can find data.

Your database layer is ready. Next up, you'll build the compute layer: a Lambda function that contains the logic to shorten URLs and redirect visitors.

Build the Compute Layer

Your database is ready to store URL mappings. Now you need code that actually processes requests. When someone sends a long URL to shorten, or visits a short code, something needs to run the logic.

You could spin up a server that runs 24/7 waiting for requests. For a URL shortener with unpredictable traffic, serverless functions are a better fit. AWS Lambda runs your code only when triggered, costs nothing when idle, and scales automatically to thousands of requests per second.

In this step, get ready to:
  • Create a Lambda function with permissions to read and write your DynamoDB table.
  • Deploy the Python code that shortens URLs and handles redirects.
Create your Lambda function
  • In the AWS Console search bar at the top, type Lambda and select the Lambda service.
  • Click Create function.
  • For Function name, enter url-shortener-function.
  • For Runtime, choose Python 3.13.
  • Expand the Change default execution role section under Permissions.
  • Select Create a new role from AWS policy templates.
  • For Role name, enter url-shortener-role.
  • For Policy templates, search for and select Simple microservice permissions.
  • Click Create function.

After a few seconds, the Lambda console loads your new function's configuration page with a code editor.

What is AWS Lambda?

Lambda is a serverless compute service. You upload code, and AWS runs it only when triggered. You never provision or manage servers.

The Simple microservice permissions policy template grants your function permission to read and write DynamoDB tables. This is how your code will access the url-shortener-links table you created in the previous step.

Why serverless instead of an always-on server?

A traditional server runs 24/7 waiting for requests, even when nobody is using your URL shortener. Lambda only runs when a request comes in, so you pay nothing during idle time.

Lambda also scales automatically. If 1,000 people hit your service at once, AWS runs 1,000 copies of your function in parallel. You do not configure this. For a URL shortener with unpredictable traffic, this is the ideal fit.

Deploy the URL shortener code

The Lambda console opened with a default code template. You will replace it with the URL shortener logic that handles two operations: creating short URLs and redirecting visitors.

  • In the code editor, select all the existing code and delete it.
  • Paste the following code into the empty editor:
import json
import boto3
import string
import random

# Connect to the DynamoDB table
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("url-shortener-links")


def generate_short_code():
    # Create a random 6-character code from letters and digits
    characters = string.ascii_letters + string.digits
    return "".join(random.choices(characters, k=6))


def lambda_handler(event, context):
    # Determine which route was called
    route_key = event.get("routeKey")

    if route_key == "POST /shorten":
        # Parse the incoming URL from the request body
        request_body = json.loads(event["body"])
        long_url = request_body["url"]
        short_code = generate_short_code()

        # Store the mapping in DynamoDB
        table.put_item(Item={
            "short_code": short_code,
            "long_url": long_url
        })

        # Build the full short URL using the API's domain
        domain = event["requestContext"]["domainName"]
        short_url = f"https://{domain}/{short_code}"

        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({
                "short_code": short_code,
                "short_url": short_url
            })
        }

    elif route_key == "GET /{shortCode}":
        # Look up the short code in DynamoDB
        short_code = event["pathParameters"]["shortCode"]
        result = table.get_item(Key={"short_code": short_code})

        if "Item" in result:
            # Redirect to the original URL
            return {
                "statusCode": 301,
                "headers": {"Location": result["Item"]["long_url"]},
                "body": ""
            }
        else:
            # Short code not found
            return {
                "statusCode": 404,
                "headers": {"Content-Type": "application/json"},
                "body": json.dumps({"error": "Short URL not found"})
            }

    return {
        "statusCode": 400,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"error": "Unsupported route"})
    }

What does this code do?

This function handles two types of requests:

  • POST /shorten: Reads the long URL from the request, generates a random 6-character short code, stores the mapping in DynamoDB, and returns the short URL to the caller.
  • GET /{shortCode}: Looks up the short code in DynamoDB. If found, it returns a 301 redirect to the original URL. If not found, it returns a 404 error.

The function uses boto3, the AWS SDK for Python, which is pre-installed in every Lambda Python runtime. You do not need to install anything extra.

  • Click the Deploy button above the code editor to save your function.
  • You should see a green success banner confirming your code was deployed.

Don't see a success message?

Make sure you deleted all the original code before pasting. Leftover code from the template can cause syntax errors that prevent deployment.

Check that your code starts with import json on the very first line with no extra spaces or blank lines above it.

Help me debug my Lambda deploy error.

✔️ Awesome, I've got everything!

Great. Make sure the success banner appeared before moving on.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py file. Compare it line by line with what you see in your Lambda code editor.

import json
import boto3
import string
import random

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("url-shortener-links")


def generate_short_code():
    characters = string.ascii_letters + string.digits
    return "".join(random.choices(characters, k=6))


def lambda_handler(event, context):
    route_key = event.get("routeKey")

    if route_key == "POST /shorten":
        request_body = json.loads(event["body"])
        long_url = request_body["url"]
        short_code = generate_short_code()

        table.put_item(Item={
            "short_code": short_code,
            "long_url": long_url
        })

        domain = event["requestContext"]["domainName"]
        short_url = f"https://{domain}/{short_code}"

        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({
                "short_code": short_code,
                "short_url": short_url
            })
        }

    elif route_key == "GET /{shortCode}":
        short_code = event["pathParameters"]["shortCode"]
        result = table.get_item(Key={"short_code": short_code})

        if "Item" in result:
            return {
                "statusCode": 301,
                "headers": {"Location": result["Item"]["long_url"]},
                "body": ""
            }
        else:
            return {
                "statusCode": 404,
                "headers": {"Content-Type": "application/json"},
                "body": json.dumps({"error": "Short URL not found"})
            }

    return {
        "statusCode": 400,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"error": "Unsupported route"})
    }
Understand the scaling power of your function

Your Lambda function is the "brain" of your system. It makes decisions, talks to the database, and returns responses to callers.

Why does this architecture scale?

If 1,000 people hit your URL shortener at the same time, AWS automatically runs 1,000 copies of your function in parallel. You did not configure anything to enable this. That is the power of serverless architecture.

Each layer of your system scales independently. DynamoDB handles millions of reads per second. Lambda runs thousands of copies in parallel. You built an architecture that could serve millions of users, and it costs nothing when idle.

Extra for Experts: Why does the code check routeKey?

The event["routeKey"] field is specific to the HTTP API (v2) event format. When API Gateway receives a request, it passes the HTTP method and path (like POST /shorten or GET /{shortCode}) as the route key. Your function uses this to decide which logic to run.

Your compute layer is deployed and ready. Next up, you will create the API layer that exposes your function to the internet so anyone can call it.

Create the API Layer

Your Lambda function is deployed and ready to process requests. But right now, nobody can reach it. The function sits inside AWS with no public address.

You need a front door that exposes your function to the internet. API Gateway fills this role. It receives HTTP requests from the outside world, routes them to your Lambda function, and returns the response to the caller.

In this step, get ready to:
  • Create an HTTP API in API Gateway.
  • Add two routes that define your API contract (POST /shorten and GET /{shortCode}).
  • Connect both routes to your Lambda function so requests flow through to your code.
Create your HTTP API

An HTTP API is the lightweight, low-cost option in API Gateway. It gives your Lambda function a public URL without the extra configuration that a REST API requires.

  • Navigate to the API Gateway console at https://console.aws.amazon.com/apigateway.
  • Choose Create API.
  • Find HTTP API and choose Build.
  • For API name, enter url-shortener-api.
  • Choose Next.
  • On the Configure routes page, choose Next to skip route creation. You will add routes manually in the next substep.
  • Review the stage that API Gateway creates for you, then choose Next.
  • Choose Create.

You now have an empty HTTP API with a public invoke URL. The next step is to define which requests it accepts.

Why HTTP API instead of REST API?

API Gateway offers two flavours: HTTP API and REST API. HTTP API is simpler to configure, cheaper, and supports the same Lambda proxy integration pattern. For a URL shortener with straightforward routing, HTTP API is the better fit.

Don't see the HTTP API option?

Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console.

help me find the HTTP API option in the API Gateway console

Add routes and connect them to Lambda

Routes define your API contract. Each route is a combination of an HTTP method and a path. You need two routes: one to create short URLs, and one to redirect visitors.

  • In the left sidebar, choose Routes.
  • Choose Create.
  • For Method, choose POST.
  • For the path, enter /shorten.
  • Choose Create.
  • Choose Create again to add a second route.
  • For Method, choose GET.
  • For the path, enter /{shortCode}.
  • Choose Create.

What do these two routes do?

POST /shorten accepts a long URL in the request body and returns a short code. GET /{shortCode} is a path parameter route. When someone visits your API with a code like abc123, API Gateway captures that value and passes it to your Lambda function.

Now you need to create a Lambda integration and attach it to both routes.

  • In the left sidebar, choose Integrations.
  • Choose Manage integrations, then choose Create.
  • Skip Attach this integration to a route for now. You will attach it in the next step.
  • For Integration type, choose Lambda function.
  • For Lambda function, enter url-shortener-function and select it from the dropdown.
  • Choose Create.

Now attach this integration to both of your routes.

  • Return to Integrations in the left sidebar.
  • Choose the POST /shorten route.
  • Under Choose an existing integration, select url-shortener-function.
  • Choose Attach integration.
  • Choose the GET /{shortCode} route.
  • Under Choose an existing integration, select url-shortener-function.
  • Choose Attach integration.

Both routes should now show that an AWS Lambda integration is attached.

Don't see your Lambda function in the dropdown?

Make sure your Lambda function url-shortener-function is in the same region (us-east-1) as your API. Functions in other regions will not appear in the dropdown.

help me connect my Lambda function to API Gateway

Find your API's live URL

Your API is now live on the internet. Every HTTP API gets a unique invoke URL that anyone can call.

  • In the left sidebar, choose your API name (url-shortener-api) to go to the Details page.
  • Find the Invoke URL field. It looks like https://abcdef123.execute-api.us-east-1.amazonaws.com.
  • Copy this URL and save it somewhere. You will use it in the next step to test your URL shortener.

What makes this architecture scalable?

API Gateway handles up to 10,000 requests per second by default, with no configuration from you. Combined with Lambda's automatic horizontal scaling, your URL shortener can handle massive traffic spikes without any changes.

This is the same role a load balancer plays in larger systems. API Gateway acts as the front door, routing traffic and protecting your backend from overload.

Your API layer is live and connected. Next up, you will test the full system end-to-end by sending real requests from your terminal.

Test Your URL Shortener

Your three layers are deployed and connected. API Gateway routes requests to Lambda, which reads and writes DynamoDB. The architecture is in place.

Now it is time to prove it works. You will send real HTTP requests to your live API endpoint and confirm the full flow: creating a short URL, storing it, and redirecting back to the original.

In this step, get ready to:
  • Send a POST request to create a shortened URL.
  • Test the redirect by visiting the short code.
  • Reflect on the complete system design you built.
Create a short URL with curl

Your API's invoke URL is the public address that connects the outside world to your system. You will send a POST request to the /shorten endpoint with a long URL in the request body.

  • In Cursor, press Ctrl+` to bring up the terminal panel.
  • Create a short URL by running this command:
curl -X POST [[INVOKE_URL="https://abcdef123.execute-api.us-east-1.amazonaws.com"]]/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://en.wikipedia.org/wiki/System_design"}'

What does this command do?

  • -X POST sends a POST request, which tells your API you want to create a new short URL.
  • -H "Content-Type: application/json" tells the API you are sending JSON data in the request body.
  • -d '{"url": "..."}' is the JSON body containing the long URL you want to shorten.

You should see a JSON response containing a short_code and a short_url.

  • Copy the short_code value from the response. You will need it for the next command.

Not seeing a JSON response?

  • Check that your invoke URL is correct. Copy it again from the API Gateway console by clicking your API name and looking at the Invoke URL on the details page.
  • Make sure both routes (POST /shorten and GET /{shortCode}) have the Lambda integration attached. In the API Gateway sidebar, click Integrations and verify both routes show your function name.
  • Why isn't my curl POST returning a response from my Lambda function?
Test the redirect

When someone visits a short URL, your system looks up the short code in DynamoDB and responds with a 301 redirect to the original URL. The browser then automatically follows that redirect.

  • Test the redirect by running this command:
curl -v [[INVOKE_URL="https://abcdef123.execute-api.us-east-1.amazonaws.com"]]/[[SHORT_CODE="your short code from the POST response"]]

What does this command do?

  • -v enables verbose mode so you can see the full HTTP response headers, including the redirect.
  • The path after your invoke URL is the short code you received from the POST response.

You should see a 301 Moved Permanently response with a Location header pointing to the Wikipedia URL. In a browser, this would automatically redirect you to the original page.

Seeing a 404 or error?

  • Double-check that you replaced the short code placeholder with the actual short_code value from your POST response.
  • If you see {"error": "Short URL not found"}, the code might have a typo. Try running the POST command again to get a fresh code.
  • Why is my short URL returning a 404 not found error?
Reflect on what you built

The complete request flow

Your URL shortener now handles the full cycle: a client sends a request, API Gateway routes it to Lambda, Lambda processes it and talks to DynamoDB, then the response returns to the client.

This is the same architecture that powers most web applications. The key scaling properties are impressive. DynamoDB handles millions of reads per second. Lambda runs thousands of copies in parallel. API Gateway handles 10,000 requests per second by default. You built an architecture that could serve millions of users, all within the free tier.

Secret mission

Add a Visit Counter

Your URL shortener works, but you have no idea how popular each link is. Can you add analytics that track how many times each short URL is visited, even when thousands of people click simultaneously?

Clean Up Your Resources

Clean Up Your Resources

All resources in this project are covered by the AWS Free Tier. For the tiny amount of usage in this project, you will not be charged. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • DynamoDB table: url-shortener-links (free tier: 25 GB storage, 25 WCU, 25 RCU).
  • Lambda function: url-shortener-function (free tier: 1 million requests per month).
  • API Gateway HTTP API: url-shortener-api (free tier: 1 million calls per month for 12 months).
  • IAM role: url-shortener-role.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your URL shortener live.

  • Your URL shortener will continue to work at zero cost as long as usage stays within free tier limits.
  • You can share your invoke URL and use it as a real URL shortener.
  • The free tier covers 1 million API calls, 1 million Lambda requests, and 25 GB of DynamoDB storage per month.

Pause - I'll come back to this later

Since all resources are serverless, they only consume resources when actively called. There is nothing to pause.

  • Lambda functions only run when triggered. They cost nothing when idle.
  • API Gateway only charges per request received. No requests means no cost.
  • DynamoDB charges for storage and throughput capacity. The free tier covers your table indefinitely at this size.
  • You can safely leave everything running and come back anytime.

Delete - I don't want to use this again

Remove all project resources permanently. Follow these steps in order.

  • Delete the API Gateway API. Open the API Gateway console at https://console.aws.amazon.com/apigateway. Select url-shortener-api. Choose Actions, then Delete. Choose Delete to confirm.
  • Delete the Lambda function. Open the Lambda console at https://console.aws.amazon.com/lambda. Open the Functions page. Select url-shortener-function. Choose Actions, Delete. Type confirm in the text input field and choose Delete.
  • Delete the DynamoDB table. Open the DynamoDB console at https://console.aws.amazon.com/dynamodb/. Select your url-shortener-links table. Choose Delete. Enter delete in the text box and choose Delete table.
  • Delete the IAM role. Open the IAM console at https://console.aws.amazon.com/iam/. In the navigation pane, choose Roles. Select the checkbox next to url-shortener-role. Choose Delete at the top of the page. Enter the role name to confirm and choose Delete.

Nice Work!

Nice Work!

You just designed and deployed a fully functional URL shortener on AWS. From an empty console to a live API that anyone on the internet can call, you built the same three-layer architecture that powers production systems everywhere.

You've learned how to:

  • Build a three-layer serverless architecture (API Gateway + Lambda + DynamoDB) that mirrors how production systems are designed.
  • Make real system design decisions: choosing NoSQL over SQL for key-value lookups, serverless over always-on servers for unpredictable traffic, and a partition key optimized for your access pattern.
  • Deploy a live URL shortener API that scales automatically to handle thousands of concurrent requests with zero configuration.
  • Secret Mission: Added atomic visit counters to track URL access, learning how systems like YouTube and Twitter handle concurrent writes at scale.

Ready to quiz yourself?