Build an Event-Driven URL Shortener
Build a serverless URL shortener on AWS with async click analytics via SQS.
Introduction
30 Second Summary
Every time you click a shortened link, a system maps a short code to the real destination in milliseconds. Behind that simple redirect is an architecture designed to handle billions of lookups while keeping analytics processing separate from the fast path.
In this project, you will build a fully functional URL shortener on AWS using serverless architecture. You'll design the same read/write separation patterns used by services handling billions of requests.
What You'll Build
A serverless URL shortener API that generates short codes, stores URL mappings in DynamoDB, and redirects visitors through API Gateway.
By the end of this project, you'll have:
- A DynamoDB table with a partition key designed for fast key-value lookups.
- Two Lambda functions handling separate read and write paths for creating and redirecting URLs.
- An API Gateway HTTP API exposing your service as a live public endpoint.
- Secret Mission: An SQS queue and analytics consumer that records click data without slowing down the redirect path.
Are there any prerequisites?
You'll need basic Python knowledge and familiarity with APIs and JSON. No prior AWS experience is required.
Set Up Your AWS Account and CLI
Your URL shortener needs a home on the internet. AWS provides the serverless infrastructure that will host your Lambda functions, DynamoDB table, and API Gateway endpoint.
In this step, you will create an AWS account, set up a dedicated user for CLI access, and install the AWS CLI so you can manage cloud resources from your terminal.
In this step, get ready to:
- Create an AWS Free Tier account and an IAM user with programmatic access.
- Install and configure AWS CLI v2 on your Linux machine.
- Verify your CLI is authenticated and ready to deploy resources.
Create an AWS Free Tier account
AWS offers a Free plan that gives you $200 in credits over 6 months. The services you will use in this project (Lambda, DynamoDB, API Gateway, SQS) all have permanent Always Free tier limits that cover low-traffic usage indefinitely.
- Open https://aws.amazon.com/free/ in your browser.
- Click Create a Free Account and follow the sign-up wizard.
- Choose the Free plan when prompted for a support plan.
- Complete email verification and payment method entry (you will not be charged on the Free plan).
Why create a separate IAM user?
Your root account has unrestricted access to everything. Best practice is to create a dedicated IAM user with only the permissions your project needs. This limits the blast radius if credentials are ever compromised.
Now create an IAM user that the CLI will authenticate as.
- Sign in to the AWS Management Console and open the IAM console at https://console.aws.amazon.com/iam/.
- In the left navigation pane, click Users.
- Click Create user.
- Enter a user name (e.g. url-shortener-cli) and click Next.
- On the Set permissions page, select Attach policies directly.
- Search for AdministratorAccess and select its checkbox.
- Click Next.
- Click Create user.
Next, generate an access key for your new user.
- Click the user name you just created to open its details page.
- Select the Security credentials tab.
- In the Access keys section, click Create access key.
- For the use case, choose Command Line Interface (CLI).
- Confirm the acknowledgment checkbox and click Next.
- Skip the description tag and click Create access key.
- Click Download .csv file and save it to a secure location. This is the only time you can view the secret access key.
- Click Done.
Keep your credentials safe
Never commit your access key ID or secret access key to version control. Never share them publicly. You will paste them into the CLI configuration in the next substep.
Install AWS CLI v2 and configure credentials
The AWS CLI lets you manage AWS services directly from your terminal. You will use it throughout this project to verify resources and test your shortener.
- Check if AWS CLI is already installed by running this command:
aws --version
✔️ I see aws-cli/2.x.x
Your AWS CLI v2 is ready. Skip ahead to the configuration step below.
ⓧ I see aws-cli/1.x.x
You have an older version installed. Upgrade to v2 by running these commands:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --update
After the install finishes, close and reopen your terminal, then run aws --version again to confirm it shows aws-cli/2.x.x.
ⓧ Command not found
AWS CLI is not installed. Install it by running these commands:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
After the install finishes, close and reopen your terminal, then run aws --version to confirm it shows aws-cli/2.x.x.
Seeing 'unzip: command not found'?
Install unzip first with sudo apt install unzip (Debian/Ubuntu) or sudo yum install unzip (Amazon Linux/RHEL), then retry the commands above.
Help me troubleshoot this installation
Now configure the CLI with the credentials from your IAM user.
- Run the configuration wizard:
aws configure
What does aws configure ask for?
The wizard prompts for four values:
- AWS Access Key ID - paste the access key ID from the .csv you downloaded.
- AWS Secret Access Key - paste the secret access key from the .csv.
- Default region name - enter us-east-1 (or your preferred region).
- Default output format - enter json.
- Enter each value when prompted, pressing Enter after each one.
Verify your CLI is authenticated
A quick identity check confirms your CLI can talk to AWS using the credentials you just configured.
- Confirm your CLI is authenticated by running:
aws sts get-caller-identity
What should I see?
You should see a JSON response containing your Account ID, UserId, and the ARN of your IAM user. Something like:
{
"UserId": "AIDAEXAMPLEID",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/url-shortener-cli"
}
Getting 'Unable to locate credentials'?
- Double-check that you ran aws configure and entered both the access key ID and secret access key correctly.
- Open ~/.aws/credentials in a text editor and verify it contains a [default] section with your keys.
- If you lost your secret key, return to the IAM console, delete the old access key, and create a new one.
Help me troubleshoot my AWS CLI credentials
Your AWS environment is ready. Next up, you will design and create the DynamoDB table that stores your URL mappings.
Design and Create the DynamoDB Table
Your AWS environment is set up and authenticated. Now it's time to create the database that powers your URL shortener.
Every URL shortener needs a fast way to look up which long URL maps to a given short code. DynamoDB is a NoSQL database built for exactly this kind of lookup. In this step, you'll design the table's key structure and create it.
In this step, get ready to:
- Create a DynamoDB table named url-shortener with a partition key designed for fast lookups.
- Understand why a partition-key-only design fits this use case.
- Verify the table is active using the AWS CLI.
Create the DynamoDB table
DynamoDB tables store data as items, and each item is identified by a primary key. For a URL shortener, the short code is the perfect primary key because every lookup says "given this short code, find me the long URL."
- In the AWS Management Console, type DynamoDB into the search bar at the top and select the DynamoDB service.
- In the left navigation pane, click Tables.
- Click Create table.
- For Table name, enter url-shortener.
- For Partition key, enter short_code and keep the type as String.
- Leave the Sort key field empty. You do not need a sort key for this table.
- For Table settings, keep the default selection of Default settings. This gives you on-demand capacity mode, which scales automatically and falls within the free tier.
- Click Create table.
The table takes a few seconds to create. Wait until the status column shows Active before moving on.
What is a partition key?
A partition key is the primary identifier DynamoDB uses to store and retrieve items. When you request an item by its partition key, DynamoDB knows exactly where to find it without scanning the entire table.
For a URL shortener, short_code is the partition key because every operation is "give me the URL for this code." This makes lookups extremely fast regardless of how many URLs are stored.
Table stuck in Creating status?
If the table stays in Creating status for more than 30 seconds, try refreshing the page. If the status shows an error, make sure your IAM user has DynamoDB permissions.
Help me troubleshoot my DynamoDB table creation
Understand the key design decisions
You might wonder why we only set a partition key and skipped the sort key. This is an intentional design choice.
Why no sort key?
DynamoDB tables can have a composite primary key (partition key + sort key) for storing multiple items under the same partition. But a URL shortener maps one short code to exactly one long URL.
This is a pure key-value lookup pattern. You ask "what is the URL for code abc123?" and get back exactly one answer. No sort key is needed because there is nothing to sort or group within a single short code.
This is the most common data access pattern in large-scale systems. Services like URL shorteners, session stores, and feature flag systems all use this single-key design for maximum speed.
Verify the table is active via the CLI
You saw the table go active in the console, but let's also confirm it with the CLI. This is a good habit for verifying infrastructure programmatically.
- Confirm the table is ready by running this command in your terminal:
aws dynamodb describe-table --table-name url-shortener
What does this command do?
The describe-table command queries DynamoDB for metadata about your table. It returns the table's key schema, capacity mode, item count, and current status.
Look for "TableStatus": "ACTIVE" in the output. This confirms DynamoDB finished creating the table and it is ready to accept reads and writes.
- You should see a JSON response containing "TableStatus": "ACTIVE" and your key schema showing short_code with KeyType of HASH.
Seeing an error about the table not found?
Make sure the table name matches exactly: url-shortener (all lowercase, with a hyphen). Also check that your CLI is configured for the same region where you created the table in the console.
Help me fix my describe-table command
Your database is ready. Next up, you'll write the Lambda function that generates short codes and stores URL mappings in this table.
Build the Create URL Lambda
Your DynamoDB table is ready and waiting for data. Now you need something to actually write to it.
In this step, you'll build the write path of your URL shortener. This is a Lambda function that receives a long URL, generates a random short code, and stores the mapping in DynamoDB. The function also handles collisions. If the randomly generated code already exists, it retries with a new one.
In this step, get ready to:
- Create a Lambda function with the Python 3.14 runtime.
- Write handler code that generates short codes and stores URL mappings in DynamoDB.
- Test the function with a mock API Gateway event.
Create the Lambda function in the console
Lambda functions are individual units of code that AWS runs on demand. You'll create one called create-url that will handle the write path of your shortener.
- In the AWS Management Console, search for Lambda in the top search bar and select it.
- Click Create function.
- Select Author from scratch.
- For Function name, enter create-url.
- For Runtime, choose Python 3.14.
- Leave architecture set to x86_64, then click Create function.
What is a Lambda function?
A Lambda function is a piece of code that AWS runs for you without any servers to manage. You upload the code, and AWS handles everything else. You only pay when the function actually executes.
The Python 3.14 runtime comes with boto3 (the AWS SDK for Python) pre-installed, so you can interact with DynamoDB without adding any external dependencies.
Now you need to give your function permission to read and write to DynamoDB. You'll attach a policy to its execution role.
- On your function's page, click the Configuration tab.
- Click Permissions in the left sidebar.
- Under Role name, click the execution role link to open it in the IAM console.
- In the Permissions policies section, click Add permissions.
- Choose Attach policies.
- Search for AmazonDynamoDBFullAccess in the search box.
- Select the checkbox next to the policy, then click Add permissions.
Why AmazonDynamoDBFullAccess?
This AWS managed policy gives your Lambda function full read and write access to all DynamoDB tables in your account. For a learning project this is fine. In production, you would create a custom policy scoped to just the url-shortener table.
Don't see the execution role link?
Make sure you're on the Configuration tab (not Code), and then click Permissions in the left sidebar. The role name appears as a blue hyperlink under Execution role.
help me find my Lambda execution role to attach the DynamoDB policy
Write the handler code
Your Lambda function needs code that parses an incoming URL, generates a unique short code, and stores the mapping in DynamoDB. You'll write this directly in the Lambda console's built-in code editor.
- Navigate back to your create-url function in the Lambda console.
- Click the Code tab.
- In the code editor, click the lambda_function.py file to open it.
- Select all the default code and delete it.
- Paste the following imports and setup code:
import json
import string
import random
import boto3
from botocore.exceptions import ClientError
# Connect to DynamoDB and reference the url-shortener table
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')
# Characters used to generate short codes (a-z, A-Z, 0-9)
CHARS = string.ascii_letters + string.digits
def generate_code(length=6):
"""Generate a random 6-character short code."""
return ''.join(random.choices(CHARS, k=length))
What does this code do?
This section sets up the DynamoDB connection and defines the short code generator.
- boto3.resource('dynamodb') creates a high-level connection to DynamoDB using the credentials from the execution role.
- CHARS contains 62 characters (letters and digits), giving you 62^6 = ~56 billion possible codes.
- generate_code() picks 6 random characters from that pool to create a short code like aB3xZ9.
- Below the generate_code function, paste the main handler:
def lambda_handler(event, context):
# Parse the JSON body from the API Gateway event
try:
body = json.loads(event.get('body', '{}'))
except (json.JSONDecodeError, TypeError):
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Invalid JSON body'})
}
# Validate that the request includes a URL
long_url = body.get('url')
if not long_url:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing "url" field'})
}
# Try up to 5 times to generate a unique short code
for _ in range(5):
code = generate_code()
try:
# Store the mapping, but only if the code doesn't already exist
table.put_item(
Item={
'short_code': code,
'long_url': long_url,
},
ConditionExpression='attribute_not_exists(short_code)'
)
return {
'statusCode': 201,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'short_code': code})
}
except ClientError as e:
# If the code already exists, try again with a new one
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
continue
raise
# If all 5 attempts collided, return an error
return {
'statusCode': 500,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Failed to generate unique code'})
}
What does lambda_handler do?
This is the entry point AWS calls when your function is invoked.
- It parses the JSON body from the incoming request and validates that a url field exists.
- It loops up to 5 times, generating a new random code each time and attempting to store it.
- The ConditionExpression='attribute_not_exists(short_code)' ensures DynamoDB only writes if that code doesn't already exist. This prevents collisions.
- If a collision occurs, DynamoDB raises ConditionalCheckFailedException and the loop tries a new code.
- In the DEPLOY section above the code editor, click Deploy to save your function.
You should see a green banner confirming the function was updated successfully.
Seeing a syntax error after deploying?
Make sure you deleted ALL the default code before pasting. If any leftover lines remain above your imports, Python will throw a syntax error.
Also check that the lambda_handler function is pasted directly below generate_code with no stray characters between them.
help me fix a syntax error in my create-url Lambda function
✔️ Awesome, I've got everything!
Great. Make sure you clicked Deploy and saw the success banner before moving on.
ⓧ I'd like to double check the full code
Here's the complete lambda_function.py file. Make sure your code editor matches this exactly:
import json
import string
import random
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')
CHARS = string.ascii_letters + string.digits
def generate_code(length=6):
return ''.join(random.choices(CHARS, k=length))
def lambda_handler(event, context):
try:
body = json.loads(event.get('body', '{}'))
except (json.JSONDecodeError, TypeError):
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Invalid JSON body'})
}
long_url = body.get('url')
if not long_url:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing "url" field'})
}
for _ in range(5):
code = generate_code()
try:
table.put_item(
Item={
'short_code': code,
'long_url': long_url,
},
ConditionExpression='attribute_not_exists(short_code)'
)
return {
'statusCode': 201,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'short_code': code})
}
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
continue
raise
return {
'statusCode': 500,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Failed to generate unique code'})
}
Test the function with a mock event
Before connecting your function to the internet, you can test it directly in the Lambda console using a mock event that simulates what API Gateway will send.
- In the TEST EVENTS section of the code editor, click Create test event.
- For Event Name, enter testCreateUrl.
- Replace the default JSON with the following test event:
{
"body": "{\"url\": \"https://www.example.com/very-long-path/that-needs-shortening\"}",
"httpMethod": "POST",
"headers": {
"Content-Type": "application/json"
}
}
Why is the body a string inside JSON?
When API Gateway sends a request to Lambda, it wraps the entire HTTP body as a JSON-escaped string inside the body field. This is why your handler uses json.loads(event.get('body', '{}')) to parse it. The test event mimics this exact format.
- Click Save.
- Click the run icon next to your test event in the TEST EVENTS section to execute the test.
You should see a response in the OUTPUT tab with statusCode 201 and a short_code value (a random 6-character string). The exact code will be different each time you run it.
Seeing an AccessDeniedException?
This means the AmazonDynamoDBFullAccess policy was not attached correctly to the execution role. Go back to Configuration > Permissions, click the role name, and verify the policy appears in the list.
Also confirm that your DynamoDB table is named exactly url-shortener and is in the same region as your Lambda function.
help me fix an AccessDeniedException in my create-url Lambda function
Your write path is live. Next up, you'll build the read path that takes a short code and redirects to the original URL.
Build the Redirect Lambda
In the previous step, you built the write path. Your create-url function accepts a long URL and stores a short code in DynamoDB.
Now you need the read path. When someone visits a short URL, your system needs to look up the code, find the original URL, and redirect the browser there. This is the hot path in any URL shortener. It must be fast and simple.
In this step, get ready to:
- Create a second Lambda function for handling redirects.
- Write handler code that looks up short codes and returns 301 redirects.
- Test the function with a mock API Gateway event.
Create the redirect-url Lambda function
Your redirect function is a separate Lambda function from the create function. Keeping read and write operations in separate functions lets each one scale independently.
- In the AWS Management Console, search for Lambda and open the Lambda service.
- Click Create function.
- Select Author from scratch.
- For Function name, enter redirect-url.
- For Runtime, choose Python 3.14.
- Leave architecture set to x86_64.
- Click Create function.
After a few seconds, the Lambda console takes you to the function's configuration page.
Why a separate function instead of one function for both?
In a real URL shortener, redirects happen far more often than URL creation. Separating them into distinct functions means you can monitor, scale, and optimize each path independently.
This is the read/write separation pattern used by services handling billions of requests.
Next, attach DynamoDB permissions to the function's execution role so it can read from your table.
- Scroll down to the Configuration tab and click Permissions in the left sidebar.
- Under Execution role, click the role name link to open it in the IAM console.
- Click Add permissions, then Attach policies.
- Search for AmazonDynamoDBFullAccess and select the checkbox next to it.
- Click Add permissions.
Don't see the role name link?
Make sure you are on the Configuration tab (not the Code tab), and then click Permissions in the left sidebar. The role name appears as a blue hyperlink under Role name.
Help me find the execution role link
Write the redirect handler code
The redirect handler receives a request with a short code in the URL path, looks it up in DynamoDB, and returns an HTTP 301 redirect to the original URL.
- Return to the redirect-url function in the Lambda console.
- Click the Code tab.
- In the code editor, select all the existing code in lambda_function.py and replace it with the following:
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')
def lambda_handler(event, context):
# Extract the short code from the URL path parameters
code = event.get('pathParameters', {}).get('code')
if not code:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing short code'})
}
# Look up the short code in DynamoDB
response = table.get_item(Key={'short_code': code})
item = response.get('Item')
if not item:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Short URL not found'})
}
# Return a 301 redirect to the original URL
return {
'statusCode': 301,
'headers': {
'Location': item['long_url'],
},
'body': ''
}
What does this code do?
event.get('pathParameters', {}).get('code') extracts the short code from the URL path. When API Gateway receives a request like GET /abc123, it passes abc123 inside pathParameters.
table.get_item(Key={'short_code': code}) performs a single-item lookup in DynamoDB using the partition key. This is the fastest possible read operation in DynamoDB.
The function returns a 301 status code with a Location header. This tells the browser to immediately navigate to the original URL.
- Click Deploy to save and deploy your function code.
- You should see a green "Successfully updated" banner at the top of the page.
Seeing a deployment error?
Make sure you replaced all the default code in lambda_function.py. If any of the original template code remains, it can cause syntax errors.
Check that your indentation uses spaces, not tabs. Python is strict about consistent indentation.
Help me fix my deployment error
✔️ Awesome, I've got everything!
Great. Double check you clicked Deploy and saw the success banner before moving on.
ⓧ I'd like to double check the full code
Here is the complete lambda_function.py for your redirect-url function:
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')
def lambda_handler(event, context):
code = event.get('pathParameters', {}).get('code')
if not code:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing short code'})
}
response = table.get_item(Key={'short_code': code})
item = response.get('Item')
if not item:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Short URL not found'})
}
return {
'statusCode': 301,
'headers': {
'Location': item['long_url'],
},
'body': ''
}
Test the function with a mock event
To verify the redirect works, you need to simulate what API Gateway will send to this function. That means crafting a test event that includes pathParameters with a short code that exists in your DynamoDB table.
- Click the Test tab in the Lambda console.
- Under Test event, select Create new event.
- For Event name, enter redirect-test.
- Replace the default JSON with the following test event:
{
"pathParameters": {
"code": [[SHORT_CODE="the short code you received from testing create-url"]]
},
"httpMethod": "GET",
"headers": {}
}
Where do I find my short code?
When you tested the create-url function in the previous step, it returned a JSON response containing a short_code value (something like aB3xYz). Use that value here.
If you do not remember it, open the DynamoDB console, navigate to your url-shortener table, click Explore table items, and copy one of the short_code values.
- Click Test.
- Under Execution result, expand Details. You should see a response with statusCode of 301 and a Location header containing your original long URL.
What should I see?
A successful test returns something like this:
{
"statusCode": 301,
"headers": {
"Location": "https://www.example.com/some-long-url"
},
"body": ""
}
The Location value should match the long URL you stored when testing create-url.
Seeing a 404 response instead?
A 404 means the short code in your test event does not match any item in DynamoDB. Check that you copied the exact code from your create-url test result. Short codes are case-sensitive.
If you see a permissions error mentioning AccessDeniedException, verify that the AmazonDynamoDBFullAccess policy is attached to this function's execution role.
Help me debug my redirect test
Your read path is working. You now have two Lambda functions: one that creates short URLs and one that redirects visitors. Next up, you will connect both functions to the internet using API Gateway so anyone can access them with a browser or an HTTP client.
Wire Up API Gateway
Your Lambda functions work perfectly in isolation. Each one does its job when triggered with a test event. But right now, nobody outside of the AWS console can reach them.
To make your URL shortener accessible from the internet, you need a front door. API Gateway is that front door. It creates a public URL that routes incoming HTTP requests to the correct Lambda function based on the method and path.
In this step, get ready to:
- Create an HTTP API in API Gateway with routes for both Lambda functions.
- Deploy the API and grab your public invoke URL.
- Test the full system end-to-end with curl.
Create an HTTP API and add routes
An HTTP API is the lightweight, low-cost option in API Gateway. It gives you a public endpoint with routing. No caching layers, no request validators. Just a clean path from the internet to your Lambda functions.
- In the AWS Management Console, search for API Gateway in the top search bar and open the service.
- Click Create API.
- Find the HTTP API card and click Build.
- On the Create an API page, skip adding integrations for now. You will add them after creating routes.
- For API name, enter url-shortener-api.
- Click Next.
- On the Configure routes page, you will see a default route. Remove it by clicking the X icon next to it.
- Click Add route. Set the method to POST and the path to /create.
- Click Add route again. Set the method to GET and the path to /{code}.
- Click Next.
- On the Define stages page, keep the default $default stage with auto-deploy enabled.
- Click Next, then click Create.
Why HTTP API instead of REST API?
API Gateway offers two flavors: REST API and HTTP API. REST API has more features like caching and request validation. HTTP API is simpler, faster, and cheaper. For a Lambda proxy integration where the function handles all the logic, HTTP API is the better fit.
Now you need to attach each route to its corresponding Lambda function.
- In the left sidebar of your new API, click Integrations.
- Click the POST /create route to select it.
- Click Create and attach an integration.
- For Integration type, choose Lambda function.
- For Lambda function, select create-url from the dropdown.
- Click Create.
- Click the GET /{code} route to select it.
- Click Create and attach an integration.
- For Integration type, choose Lambda function.
- For Lambda function, select redirect-url from the dropdown.
- Click Create.
What does this wiring do?
Each route maps an HTTP method and path to a specific Lambda function. When someone sends a POST to /create, API Gateway invokes create-url. When someone sends a GET to /{code}, API Gateway invokes redirect-url with the path parameter available in the event.
This is read/write path separation in action. The write path (creating short URLs) and the read path (redirecting) are handled by completely different functions.
Don't see your Lambda functions in the dropdown?
Make sure you are in the same AWS region where you created your Lambda functions. The region selector is in the top-right corner of the console.
help me find my Lambda functions in the API Gateway integration dropdown
Deploy the API and copy the invoke URL
Because you chose auto-deploy during creation, your API is already live. Every change you make is automatically deployed to the $default stage. All you need to do is grab the URL.
- In the left sidebar, click API: url-shortener-api to return to the API overview.
- Find the Invoke URL displayed on the overview page. It looks like https://<api-id>.execute-api.<region>.amazonaws.com.
- Copy this URL. You will use it in the next substep to test your shortener.
What is a stage?
A stage is a named deployment of your API. The $default stage means requests go directly to the base URL without a stage path prefix. Auto-deploy means any route or integration change goes live immediately.
Test end-to-end with curl
This is the moment of truth. You will send a real HTTP request from your terminal to your public API, which will hit Lambda, which will read and write to DynamoDB. The full serverless chain.
- Create a short URL by running this command in your terminal (replace the invoke URL with your own):
curl -X POST [[INVOKE_URL="your API invoke URL"]]/create \
-H "Content-Type: application/json" \
-d '{"url": "https://www.example.com/some-very-long-path"}'
What does this command do?
curl -X POST sends an HTTP POST request to your API Gateway endpoint at /create.
-H sets the Content-Type header so Lambda knows to parse the body as JSON. -d sends the JSON body containing the long URL you want to shorten.
You should see a response like this:
{"short_code": "aB3xK9"}
Your short code will be different since it is randomly generated. Copy it for the next test.
- Test the redirect by running this command (replace the short code with the one you received):
curl -v [[INVOKE_URL="your API invoke URL"]]/[[SHORT_CODE="the short code from the previous response"]]
What does this command do?
The -v flag shows verbose output including response headers. You need this to see the 301 redirect and the Location header pointing to your original long URL. Without -v, curl would silently follow the redirect and you would not see the header.
In the verbose output, look for these lines:
< HTTP/2 301
< location: https://www.example.com/some-very-long-path
The 301 status and the Location header confirm that your redirect Lambda is working correctly through API Gateway.
Getting an Internal Server Error?
A 500 error usually means your Lambda function crashed. Check the Lambda console for recent invocation errors in the Monitor tab of each function. Common causes:
- The Lambda execution role is missing AmazonDynamoDBFullAccess.
- The DynamoDB table name in your code does not match url-shortener.
- Your curl request is missing the Content-Type header on the POST.
help me debug a 500 error from my API Gateway Lambda integration
Getting a 404 Not Found?
This means API Gateway could not match your request to a route. Double check that:
- Your POST request includes /create at the end of the invoke URL.
- Your GET request uses a forward slash before the short code (not a question mark).
- The route path in API Gateway is exactly /{code} (with curly braces).
help me fix a 404 from API Gateway when my route should exist
Secret mission
Add Async Click Analytics via SQS
Your URL shortener works, but you have no idea how many times people click each link. In production systems like Bitly, analytics runs separately from the redirect path so that counting clicks never slows down the user. Can you add a message queue that decouples click tracking from the fast redirect?
Clean Up Your Resources
Clean Up Your Resources
All the resources in this project are serverless. They only incur charges when invoked, and normal usage stays well within AWS Always Free tier limits.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- DynamoDB table url-shortener.
- Lambda function create-url.
- Lambda function redirect-url.
- Lambda function process-clicks (Secret Mission).
- HTTP API in API Gateway.
- SQS queue click-events (Secret Mission).
- IAM user with programmatic access.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your URL shortener.
- Lambda, DynamoDB, SQS, and API Gateway all have permanent Always Free tier allowances. Lambda gives you 1 million requests per month. DynamoDB gives you 25 GB of storage. SQS gives you 1 million requests per month. API Gateway gives you 1 million HTTP API calls per month for 12 months.
- Your resources will not generate charges unless you exceed these limits, which is unlikely for a learning project.
- Your IAM user and AWS CLI configuration remain active on your machine.
Pause - I'll come back to this later
Serverless resources only incur costs when invoked. There is nothing to pause because nothing runs while idle.
- Your Lambda functions, DynamoDB table, API Gateway API, and SQS queue all sit dormant at zero cost until you invoke them again.
- If you want to prevent anyone from calling your API while you are away, you can delete the API Gateway stage. This disables the invoke URL without removing your Lambda functions or data.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps in order.
- Delete the HTTP API: Open the API Gateway console. On the APIs page, select your API. Choose Actions, then Delete. Choose Delete to confirm.
- Delete the Lambda functions: Open the Functions page of the Lambda console. Select create-url. Choose Actions, Delete. Type confirm in the text input field and choose Delete. Repeat for redirect-url and process-clicks.
- Delete the DynamoDB table: Open the Tables page of the DynamoDB console. Select the url-shortener table. Choose Delete. Enter delete in the text box. Choose Delete table.
- Delete the SQS queue: Open the Amazon SQS console. In the navigation pane, choose Queues. Select the click-events queue. Choose Delete. In the dialog box, enter delete to confirm. Choose Delete.
- Delete the IAM user (optional): Open the IAM console. In the navigation pane, choose Users. Select the checkbox next to the IAM user you created for this project. Choose Delete at the top of the page. Enter the username to confirm and choose Delete.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional URL shortener service running on AWS with the same architectural patterns used by services handling billions of requests.
You've learned how to:
- Built a serverless URL shortener with separate read and write Lambda functions, each with a single responsibility.
- Designed a DynamoDB table with a partition key optimized for pure key-value lookups.
- Connected Lambda functions to the internet via an API Gateway HTTP API with route-level integrations.
- Secret Mission: Decoupled click tracking from the redirect path using SQS, demonstrating asynchronous event processing to keep the hot path fast.
Ready to quiz yourself?