Build a Serverless URL Shortener
Build a URL shortener API using Lambda, API Gateway, and DynamoDB.
Introduction
30 Second Summary
Every time you share a link on social media or in a text message, there is a good chance it has been shortened. Services like bit.ly take impossibly long URLs and turn them into short, shareable codes.
In this project, you will build your own URL shortener on AWS using a fully serverless architecture. You will create a live API that turns long URLs into short codes and redirects anyone who clicks them to the original destination.
What You'll Build
You send a long URL to your live API, get back a short code, and when you paste that code into any browser it redirects straight to the original page.
By the end of this project, you'll have:
- A working API endpoint where you send any long URL and get back a short code you can share with anyone.
- A redirect system that sends anyone who visits your short code straight to the original destination in their browser.
- A DynamoDB table where you can browse every shortened URL and its mapping, with the entire system running without a single server to manage.
- Secret Mission: Add link expiration so your short URLs automatically stop working after a configurable time period.
Do I need any prerequisites?
You need an AWS account with free tier access and a web browser. If you do not have an AWS account yet, you can create one for free at aws.amazon.com.
Before We Start
Before we start building, let's get clear on what you're creating and why. Understanding the goal upfront makes every step that follows click into place.
Set Up DynamoDB and Your Lambda Function
Before you can shorten a single URL, you need two things: a place to store the mapping between short codes and long URLs, and a piece of compute logic to create those mappings.
In this step, you will create a DynamoDB table for storage and a Lambda function for compute. Together, they form the serverless backbone of your URL shortener.
In this step, get ready to:
- Create a DynamoDB table to store URL mappings.
- Create a Lambda function and give it permission to access DynamoDB.
- Write and test code that creates short URLs.
Create a DynamoDB table
Your URL shortener needs a database to map short codes to their original URLs. DynamoDB is a key-value database that is perfect for this. You give it a short code, it hands back the URL.
- In the AWS Management Console search bar, type DynamoDB and open the DynamoDB console.
- In the left navigation pane, choose Tables.
- Click Create table.
- For Table name, enter url-shortener.
- For Partition key, enter short_code and make sure the type dropdown is set to String.
- Leave Sort key empty.
- Under Table settings, keep the default selection of Default settings.
- Click Create table.
Wait a few seconds until the table status changes to Active. You should see url-shortener listed in your tables with a green Active status.
What is a partition key?
A partition key is the primary way DynamoDB identifies each item in your table. Every item must have a unique partition key value. In your case, each short code (like aBc123) is unique, so it works perfectly as the partition key.
Why DynamoDB instead of a traditional database?
A URL shortener is fundamentally a key-value lookup. You give it a short code, it returns a URL. DynamoDB is built for exactly this kind of access pattern. It is serverless, scales automatically, and has a generous free tier.
A relational database like RDS would require provisioning an instance and managing capacity. That adds cost and complexity for a task that does not need relational features.
Create a Lambda function
Now you need a function to run your URL shortening logic. Lambda lets you run code without provisioning a server. It only runs when called and costs nothing when idle.
- In the AWS Management Console search bar, type Lambda and open the Lambda console.
- Click Create function.
- Select Author from scratch.
- For Function name, enter url-shortener-function.
- For Runtime, choose Python 3.13.
- Leave everything else at default and click Create function.
Lambda creates your function and opens the code editor. You should see a file called lambda_function.py with some starter code.
What is an execution role?
When Lambda created your function, it also created an IAM execution role. This role controls what AWS services your function is allowed to talk to. Right now, it only has permission to write logs. You need to add DynamoDB permissions next.
Your function needs permission to read from and write to your DynamoDB table. You will attach a managed policy to the function's execution role.
- In your Lambda function page, click the Configuration tab.
- Click Permissions in the left sidebar.
- Under Execution role, click the role name link. This opens the IAM console in a new tab.
- In the IAM console, click Add permissions, then click Attach policies.
- Search for AmazonDynamoDBFullAccess.
- Select the checkbox next to it and click Add permissions.
You should see AmazonDynamoDBFullAccess listed under the role's policies. Your function can now read and write to DynamoDB.
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 clickable blue link under Execution role.
Help me find the execution role link for my Lambda function.
Write your URL shortening code
Your Lambda function currently has placeholder code that Lambda generated. You will replace it with code that accepts a long URL, generates a random short code, and stores the mapping in DynamoDB.
- Switch back to the Lambda console tab in your browser.
- Click the Code tab to open the code editor.
- Select all the existing code in lambda_function.py and delete it.
- Paste in the first block of code, which sets up your imports, DynamoDB connection, and handler routing:
import json
import string
import random
import time
import boto3
# Connect to the DynamoDB url-shortener table
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('url-shortener')
def generate_short_code(length=6):
# Build a pool of letters and digits, then pick 6 at random
characters = string.ascii_letters + string.digits
return ''.join(random.choices(characters, k=length))
def lambda_handler(event, context):
# Read the HTTP method from the incoming request
http_method = event.get('httpMethod', '')
# Route POST requests to the create handler
if http_method == 'POST':
return handle_create(event)
else:
return {
'statusCode': 405,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Method not allowed'})
}
What does this code do?
- The imports bring in Python's built-in modules plus boto3, the AWS SDK for Python. Boto3 comes pre-installed in every Lambda Python runtime.
- dynamodb.Table('url-shortener') connects your code to the DynamoDB table you just created.
- generate_short_code() picks 6 random characters from letters and digits to create a unique short code.
- lambda_handler is the entry point Lambda calls when your function is invoked. It checks the HTTP method and routes POST requests to handle_create.
- Click Deploy to save your progress.
You should see a green banner confirming your changes were deployed. The function is not complete yet, but saving now ensures your work is not lost.
- Now paste the handle_create function below your existing code. This function parses the incoming request and validates the URL:
def handle_create(event):
# Parse the JSON request body
try:
body = json.loads(event.get('body', '{}') or '{}')
except json.JSONDecodeError:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Invalid JSON in request body'})
}
long_url = body.get('long_url', '').strip()
# Reject the request if no URL was provided
if not long_url:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing required field: long_url'})
}
What does this code do?
- The try/except block safely parses the JSON body. If someone sends broken JSON, the function returns a clear 400 error instead of crashing.
- body.get('long_url', '').strip() extracts the URL from the request and removes any whitespace.
- If long_url is empty or missing, the function returns a 400 error with a helpful message.
- Paste the core storage logic directly below the validation code you just added. This part generates the short code, stores it, and returns the result:
# Generate a short code and save the mapping to DynamoDB
short_code = generate_short_code()
table.put_item(
Item={
'short_code': short_code,
'long_url': long_url,
'created_at': int(time.time())
}
)
# Return the new short code to the caller
return {
'statusCode': 201,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({
'short_code': short_code,
'long_url': long_url
})
}
What does this code do?
- generate_short_code() creates a random 6-character code like xK9mPq.
- table.put_item() writes a new item to DynamoDB with the short code, the original URL, and a timestamp.
- The function returns a 201 status code ("Created") with the short code and original URL in the response body.
- Make sure the handle_create indentation is correct. The lines starting with short_code = generate_short_code() should be indented inside handle_create, at the same level as the if not long_url: block above.
- Click Deploy to save your complete code.
You should see the green deployment confirmation banner again.
✔️ Awesome, I've got everything!
Great. Make sure you have saved your file by clicking Deploy.
ⓧ I'd like to double check the full code
Scroll up through your code editor and confirm these three sections are present, in order:
- Imports, DynamoDB connection, and generate_short_code function (the first block you pasted).
- lambda_handler function that checks the HTTP method and routes to handle_create (also in the first block).
- handle_create function with JSON parsing, long_url validation, short code generation, table.put_item(), and a 201 response (the second and third blocks you pasted).
If anything looks off, revisit the code blocks above and re-paste the missing section.
Now it is time to test your function. Before you run the test, think about this: if you send a POST request with a long_url of https://example.com, what status code do you expect to get back?
- In the Lambda console, click the Test tab.
- For Event name, enter testPost.
- Replace the default JSON with the following test event:
{
"httpMethod": "POST",
"body": "{\"long_url\": \"https://example.com\"}"
}
- Click Save, then click Test.
You should see a green Execution result: succeeded banner. Expand the details and look at the response body. You will see a statusCode of 201 and a short_code value like xK9mPq (your code will be different since it is randomly generated).
Seeing an error instead?
If you see an AccessDeniedException, the AmazonDynamoDBFullAccess policy was not attached correctly. Go back to the Configuration tab, click Permissions in the left sidebar, click the role name, and verify the policy is listed.
If you see a syntax error, check that the indentation in your code is consistent. Python is sensitive to indentation levels.
Help me debug this Lambda function error.
Now confirm the item was actually stored in DynamoDB.
- Open the DynamoDB console in a new tab.
- In the left navigation pane, click Explore items.
- Select the url-shortener table.
You should see one item in the table with a short_code, a long_url of https://example.com, and a created_at timestamp. Your Lambda function successfully wrote to DynamoDB.
You now have a working database and a function that creates short URLs. In the next step, you will add the ability to redirect. When someone visits a short code, your function will look it up and send them to the original URL.
Make Your First Short URL Work End-to-End
In the previous step, you built a Lambda function that creates short codes and stores them in DynamoDB. That is half the job done.
The other half is making those short codes actually do something. Right now, if someone has a short code, there is no way to use it to get back to the original URL. Your function needs to handle GET requests, look up the short code, and redirect the visitor. In this step, you will add that redirect logic and handle the case where a short code does not exist.
In this step, get ready to:
- Add GET request handling that looks up short codes and redirects to the original URL.
- Add error handling that returns a 404 when a short code does not exist.
Add GET request handling with redirect logic
Your lambda_handler function currently only handles POST requests. When any other HTTP method comes in, it returns a 405 "Method not allowed" error. You need to add a GET path so the function can look up short codes and redirect visitors.
- Switch back to the Lambda console tab with your url-shortener-function from the previous step.
- In the code editor, find the else: block inside lambda_handler that returns the 405 error. It looks like this:
else:
return {
'statusCode': 405,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Method not allowed'})
}
- Replace that entire else: block with the following, which adds an elif branch for GET requests before the else: fallback:
elif http_method == 'GET':
return handle_redirect(event)
else:
return {
'statusCode': 405,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Method not allowed'})
}
What does this change do?
You are adding a new route to your function. When httpMethod is GET, the function now calls a handle_redirect function (which you will write next). POST requests still go to handle_create, and anything else still gets a 405.
Now you need to write the handle_redirect function itself. This function extracts the short code from the URL path, looks it up in DynamoDB, and returns an HTTP 301 redirect with the original URL.
- Scroll to the bottom of lambda_function.py, below the entire handle_create function, and add the following code:
def handle_redirect(event):
# Extract the short code from the URL path
path_params = event.get('pathParameters') or {}
short_code = path_params.get('proxy', '').strip('/')
if not short_code:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'No short code provided'})
}
# Look up the short code in DynamoDB
response = table.get_item(Key={'short_code': short_code})
item = response['Item']
long_url = item['long_url']
# Return a 301 redirect to the original URL
return {
'statusCode': 301,
'headers': {
'Location': long_url
},
'body': ''
}
What does this code do?
This function handles the redirect side of your URL shortener. It pulls the short code from the URL path using pathParameters. The proxy key is how API Gateway passes the path segment when you set up a proxy resource later.
It then calls table.get_item() to look up the short code in your url-shortener DynamoDB table. If the item exists, it returns a 301 status code with a Location header pointing to the original URL. Browsers automatically follow 301 redirects, so the visitor lands on the original page.
- Click Deploy to save and deploy your updated function.
Time to test. You need a real short code that already exists in your DynamoDB table from Step 1.
- Open the DynamoDB console in a new tab.
- Click Tables in the left sidebar, then click url-shortener.
- Click Explore table items.
- Copy the short_code value from the item you created in Step 1. You will need this for your test event.
Keep DynamoDB open
Leave this DynamoDB tab open. You will come back to it later to verify new items appear in your table.
- Switch back to the Lambda console tab with url-shortener-function.
- Click the Test tab.
- In the Event JSON section, replace the existing event with the following, substituting your real short code:
{
"httpMethod": "GET",
"pathParameters": {
"proxy": "[[SHORT_CODE=\"your short code from Step 1\"]]"
}
}
Before you click Test, think about what you expect to see. The function should look up this short code in DynamoDB and return a redirect. What status code and headers do you predict?
- Click Test.
Under Execution result, expand Details. You should see a response with statusCode: 301 and a Location header pointing to the original URL you shortened in Step 1.
Seeing an error instead of a 301?
- Make sure you replaced the placeholder in the test event with the actual short_code value from your DynamoDB table. The code is case-sensitive.
- Confirm you clicked Deploy after editing your code. Changes are not live until you deploy.
- Check that your handle_redirect function is outside and below handle_create. It should be a separate top-level function, not nested inside another function.
Help me debug my Lambda redirect function.
Your redirect works for short codes that exist. But what happens when someone uses a short code that is not in your database?
- In the Event JSON section, replace the previous event with a fake short code:
{
"httpMethod": "GET",
"pathParameters": {
"proxy": "fakecode123"
}
}
Before you click Test, predict what will happen. The function tries to access response['Item'] directly. What happens when DynamoDB does not find a matching item?
- Click Test.
The function fails. Without a check for missing items, the code crashes when it tries to access a key that does not exist in the response. This is exactly the gap you will fix next.
Add error handling for missing short codes
A production API should never crash on bad input. Instead of letting the function error out, you need to check whether DynamoDB actually found the item before trying to use it. If the short code does not exist, the function should return a clear 404 response.
- In the code editor, find this line inside the handle_redirect function:
item = response['Item']
- Add the following 404 check directly above that line, between table.get_item() and item = response['Item']:
if 'Item' not in response:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Short URL not found'})
}
What does this check do?
When table.get_item() does not find a matching item, the response dictionary simply does not contain an Item key. This check catches that case and returns a 404 status code with a helpful error message, instead of letting the function crash.
- Click Deploy to save and deploy your updated function.
The test event with fakecode123 should still be loaded from your last test. Time to see if the fix works.
- Click Test.
Under Execution result, expand Details. This time you should see a clean response with statusCode: 404 and the error message Short URL not found.
Still seeing an error instead of a 404?
- Make sure the if 'Item' not in response: block is placed above item = response['Item']. The check must happen before the code tries to access the item.
- Double-check that you clicked Deploy after adding the new code.
Help me fix my 404 error handling in the Lambda function.
✔️ Awesome, I've got everything!
Great. Double check you have clicked Deploy after all your edits.
ⓧ I'd like to double check the full code
Your lambda_function.py should contain these sections in order. Scroll through your code editor and confirm each one is present:
- Imports: json, string, random, time, boto3.
- DynamoDB setup: dynamodb = boto3.resource('dynamodb') and table = dynamodb.Table('url-shortener').
- The generate_short_code function that creates a random 6-character string.
- The lambda_handler function with three branches: POST calls handle_create, GET calls handle_redirect, and everything else returns 405.
- The handle_create function that parses the body, generates a short code, and stores it in DynamoDB.
- The handle_redirect function that extracts the short code from pathParameters, checks for missing items with a 404 response, and returns a 301 redirect with the Location header.
Your URL shortener now handles both sides of the equation. It can create short codes and redirect visitors to the original URL. But right now, it is trapped inside the Lambda console. In the next step, you will expose it to the world with a public API endpoint that anyone can use.
Expose Your API to the World
Your URL shortener works perfectly inside the Lambda console. You can create short codes and look up URLs. There is just one problem: it is trapped.
Nobody outside AWS can use it because there is no public HTTP endpoint. In this step, you will connect your Lambda function to API Gateway, which creates a public URL that anyone can call from a browser or curl.
In this step, get ready to:
- Create a REST API in API Gateway with Lambda proxy integration.
- Deploy the API to a public stage.
- Test the full flow from curl and a browser.
Create a REST API in API Gateway
API Gateway is the front door for your Lambda function. It receives HTTP requests from the internet and forwards them directly to your function using something called Lambda proxy integration.
- Open the API Gateway console at https://console.aws.amazon.com/apigateway.
- Choose Create API.
- Find REST API (not the Private option) and choose Build.
- For API name, enter url-shortener-api.
- Keep API endpoint type set to Regional.
- For IP address type, select IPv4.
- Choose Create API.
What is Lambda proxy integration?
In a Lambda proxy integration, API Gateway takes the entire incoming HTTP request and passes it straight to your Lambda function as the event object. Your function reads httpMethod, pathParameters, and body from that event to decide what to do.
The function then returns a response with statusCode, headers, and body, and API Gateway forwards it back to the caller. No mapping templates needed.
You should now see the Resources pane with a root resource /. Next, you will create a proxy resource that catches all URL paths.
Create the proxy resource and connect it to Lambda
A proxy resource uses a special path called {proxy+} that catches every URL path after the root. This means any short code in the URL (like /abc123) gets routed to your Lambda function automatically.
- In the Resources pane, choose Create resource.
- Turn on Proxy resource.
- Keep Resource path as /.
- The Resource name auto-fills to {proxy+}. Leave it as is.
- Keep CORS (Cross Origin Resource Sharing) turned off.
- Choose Create resource.
You should see /{proxy+} appear under the root resource with an ANY method already created. The ANY method is a placeholder that matches all HTTP methods (GET, POST, PUT, DELETE, and so on).
Now connect this ANY method to your Lambda function.
- Select the /{proxy+} resource, then choose the ANY method.
- Under the warning symbol, choose Edit integration.
- For Integration type, select Lambda function.
- Turn on Lambda proxy integration.
- For Lambda function, enter url-shortener-function and select it from the dropdown.
- Keep Default timeout turned on.
- Choose Save.
Why a proxy resource?
Without {proxy+}, you would need to create a separate API resource for every possible short code. The proxy resource catches all paths in a single setup, so /abc123, /xyz789, and any future short code all route to your Lambda function.
Your Lambda function receives the path as event['pathParameters']['proxy'] and uses it to look up the matching URL in DynamoDB.
The proxy resource handles paths like /abc123, but POST requests to the root path / (to create new short URLs) would not match {proxy+}. You need to add an integration on the root resource too.
- Select the / root resource, then choose Create method.
- For Method type, select ANY.
- For Integration type, select Lambda function.
- Turn on Lambda proxy integration.
- For Lambda function, enter url-shortener-function and select it from the dropdown.
- Choose Create method.
API Gateway handles permissions automatically
When you connect a Lambda function through the API Gateway console, it automatically adds a resource-based policy to your Lambda function. This grants API Gateway permission to invoke your function. You do not need to configure any IAM permissions manually.
Your Resources pane should now show the / root with an ANY method, and /{proxy+} underneath it with its own ANY method. Both point to url-shortener-function.
Deploy the API and test the full flow
Your API exists, but it is not live yet. You need to deploy it to a stage to get a public URL that anyone can call.
- In the Resources pane, choose Deploy API.
- For Stage, select New stage.
- For Stage name, enter prod.
- Choose Deploy.
After deployment, you will see the Stage details page.
- From the main navigation pane, choose Stage.
- Under Stage details, find the Invoke URL. Click the copy icon to copy it.
- Save this URL somewhere handy. It will look like:
https://your-api-id.execute-api.your-region.amazonaws.com/prod
What is a stage?
A stage is a named snapshot of your API deployment. The stage name becomes part of the URL. You called yours prod, so the URL ends with /prod. You could create separate stages like dev or test for different environments.
Now for the moment of truth. You have a public URL. Before you test it, take a moment to predict what will happen.
If you send a POST request with a long URL, what do you think the response will contain? What HTTP status code should come back?
- Open a terminal on your computer by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Create a short URL by running this command (replace the URL with your own invoke URL):
curl -X POST [[INVOKE_URL="your invoke URL from the Stage details page"]]/prod -H "Content-Type: application/json" -d '{"long_url": "https://aws.amazon.com"}'
You should get back a 201 response with a JSON body containing your short_code and the original long_url.
What does the response look like?
The response will be something like:
{"short_code": "aB3xY9", "long_url": "https://aws.amazon.com"}
Your short_code will be a random 6-character string. That is the code your Lambda function generated and stored in DynamoDB.
Now test the redirect. Copy the short_code from the response and try visiting it in your browser.
- Open your web browser and paste your invoke URL followed by / and your short code. For example:
https://your-api-id.execute-api.your-region.amazonaws.com/prod/aB3xY9
Your browser should redirect you to https://aws.amazon.com. Your Lambda function looked up the short code in DynamoDB, found the matching URL, and returned a 301 redirect. The browser followed the Location header automatically.
Not getting redirected?
- Make sure you copied the full invoke URL including https:// and the /prod stage at the end. The short code goes after /prod/.
- Double-check you are using a short code from the POST response, not a made-up value.
- If you see a 502 error, your Lambda function might not be returning the response in the format API Gateway expects. Go back to the Lambda console and verify your function code returns statusCode, headers, and body keys.
- Help me troubleshoot why my API Gateway URL is not redirecting to the original URL.
Your URL shortener is live. Anyone with the invoke URL can shorten a link and get redirected. Next up, you will add input validation and duplicate detection to make the API production-ready.
Handle Edge Cases and Make It Robust
Your URL shortener works for the happy path. A valid URL goes in, a short code comes out, and redirects land where they should.
But what happens when someone sends garbage input or shortens the same URL five times? Right now, the API happily accepts both. In this step, you'll add input validation to reject malformed URLs and duplicate detection to avoid wasting storage on repeated entries.
In this step, get ready to:
- Add URL format validation to reject malformed input.
- Create a DynamoDB Global Secondary Index for duplicate detection.
- Test edge cases with curl to confirm the API handles bad input gracefully.
Add input validation to reject malformed URLs
Before you build anything new, try sending your current API a URL that is not actually a URL. What do you think will happen?
- Test your API with a fake URL by running this command:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "not-a-real-url"}'
You get back a 201 response with a short code. The API stored not-a-real-url as if it were a legitimate URL. If someone tries to follow that short link, the redirect goes nowhere useful.
Your API needs a way to check whether the incoming string actually looks like a URL before storing it. You'll add a validation function that uses a regular expression to enforce this.
- Return to the url-shortener-function in the Lambda console.
- In the code editor, find the import lines at the top of your file. Add import re after import time.
- Below the generate_short_code function, add this new validation function:
def is_valid_url(url):
# Check if the URL matches a valid http:// or https:// pattern
pattern = re.compile(
r'^https?://'
r'(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*'
r'[a-zA-Z]{2,}'
r'(?::\d{1,5})?'
r'(?:/[^\s]*)?$'
)
return bool(pattern.match(url))
What does this function do?
The is_valid_url function uses a regular expression to check whether a string looks like a real URL. It requires the string to start with http:// or https://, followed by a valid domain name.
Strings like not-a-real-url or just some text will fail this check. Strings like https://aws.amazon.com will pass.
Now you need to wire this function into the handle_create function so it actually runs on incoming requests.
- In the handle_create function, find this existing block:
if not long_url:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Missing required field: long_url'})
}
- Directly below that block, add this URL format validation check:
if not is_valid_url(long_url):
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Invalid URL format. URL must start with http:// or https://'})
}
What does this check do?
This runs is_valid_url() on the incoming long_url before any DynamoDB writes happen. If the URL format is invalid, the function returns a 400 response immediately and never touches the database.
- Click Deploy in the Lambda code editor to save your changes.
- Test the validation by running this command with the same fake URL:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "not-a-real-url"}'
This time you get a 400 response with the message Invalid URL format. URL must start with http:// or https://. The garbage input is rejected before it reaches DynamoDB.
Still getting a 201 response?
- Make sure you clicked Deploy in the Lambda console after adding the code. Saving the file is not enough.
- Check that the is_valid_url check is placed after the empty-string check for long_url, not before it.
- Verify that import re is at the top of your file. The re.compile call inside the function depends on this import.
Why isn't my Lambda URL validation working after deploying?
Create a GSI and add duplicate detection
Your API now rejects bad URLs. But there is another gap. Try shortening the same valid URL twice and see what happens.
- Shorten a valid URL by running this command:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "https://aws.amazon.com"}'
- Run the exact same command again.
You get two different short codes for the same URL. Every time someone shortens https://aws.amazon.com, a brand new entry is created. That wastes storage and makes it impossible to track all traffic through a single short link.
To fix this, you need to check whether a URL has already been shortened before creating a new entry. The problem is that your table's partition key is short_code. Looking up by long_url would require scanning every item in the table, which is slow and expensive. A Global Secondary Index solves this by creating a separate lookup path keyed on long_url.
What is a Global Secondary Index?
A Global Secondary Index (GSI) is like building a second table of contents for the same data. Your main table is organized by short_code. The GSI reorganizes the same data by long_url, so you can quickly find an item by either attribute.
DynamoDB keeps the GSI automatically in sync with your table. Every time you add or delete an item, the GSI updates too.
- In the AWS Management Console, navigate to the DynamoDB service.
- Click Tables in the left sidebar, then click your url-shortener table.
- Select the Indexes tab.
- Click Create index.
- Enter long_url as the partition key.
- Leave the sort key empty.
- Enter long_url-index as the index name.
- Click Create index.
The index status shows Creating. Wait until it changes to Active before continuing. For a small table this takes about a minute.
Index stuck in Creating status?
DynamoDB needs to backfill existing data into the new index. For a table with only a few items, this is fast. Refresh the page after a minute. If the status has not changed after several minutes, check that you entered long_url as the partition key (not a misspelled attribute name).
Why is my DynamoDB GSI stuck in Creating status?
Now add the duplicate detection logic to your Lambda function.
- Return to the url-shortener-function code editor in the Lambda console.
- At the top of your file, find import boto3. Add this new import directly below it:
from boto3.dynamodb.conditions import Key
- In the handle_create function, find the line short_code = generate_short_code(). Add this duplicate detection block directly above that line:
# Check if this URL has already been shortened
existing = table.query(
IndexName='long_url-index',
KeyConditionExpression=Key('long_url').eq(long_url)
)
if existing['Items']:
item = existing['Items'][0]
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({
'short_code': item['short_code'],
'long_url': long_url,
'message': 'URL already shortened'
})
}
What does this code do?
Before generating a new short code, the function queries the long_url-index GSI to check if this URL has been shortened before. If a match is found, it returns the existing short code with a 200 status instead of creating a duplicate entry.
The Key('long_url').eq(long_url) expression is a DynamoDB condition that matches items where the long_url attribute equals the incoming URL. The GSI makes this lookup fast regardless of how many items are in the table.
- Click Deploy to save your changes.
- Test duplicate detection by shortening the same URL twice:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "https://example.com/test"}'
- Run the same command a second time.
The first request returns a 201 with a new short code. The second returns a 200 with the same short code and the message URL already shortened. No duplicate entry created.
Getting a 500 error on the second request?
- Make sure the GSI status is Active in the DynamoDB console before testing. Querying a GSI that is still creating causes an error.
- Verify the index name in your code is long_url-index. It must match the name you entered in the DynamoDB console exactly.
- Check that from boto3.dynamodb.conditions import Key is at the top of your file. Without this import, the Key() call will fail.
Why is my Lambda function returning a 500 error when querying the DynamoDB GSI?
✔️ Awesome, I've got everything!
Make sure you've saved your work by clicking Deploy in the Lambda console.
ⓧ I'd like to double check the full code
Your lambda_function.py is now over 90 lines. Scroll through your code in the Lambda editor and confirm these sections are present, in this order:
- Imports: json, string, random, time, re, boto3, and from boto3.dynamodb.conditions import Key.
- DynamoDB table setup: dynamodb = boto3.resource('dynamodb') and table = dynamodb.Table('url-shortener').
- The generate_short_code function.
- The is_valid_url function with the regex pattern.
- The lambda_handler function routing POST and GET requests.
- The handle_create function with JSON parsing, empty long_url check, is_valid_url check, GSI duplicate query, and put_item for new URLs.
- The handle_redirect function with 301 redirect and 404 handling.
Test your edge cases
Your API now validates input and detects duplicates. Before you wrap up, run a final battery of tests to confirm every edge case is covered.
- Before you run these tests, predict which ones will return a 400 error and which will succeed.
- Test with an empty body by running:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{}'
You get a 400 with Missing required field: long_url.
- Test with an invalid URL format by running:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "definitely-not-a-url"}'
You get a 400 with Invalid URL format. URL must start with http:// or https://.
- Test duplicate detection by shortening the same URL twice:
curl -X POST [[INVOKE_URL="https://your-api-id.execute-api.your-region.amazonaws.com/prod"]] -H "Content-Type: application/json" -d '{"long_url": "https://aws.amazon.com"}'
- Run the same command again.
Both responses return the same short_code. The second response includes the message URL already shortened and a 200 status code instead of 201.
Getting unexpected results?
- If the empty body test returns a 201, check that the if not long_url check is still in your handle_create function. It was there from an earlier step.
- If the duplicate test returns two different short codes, make sure the GSI query block is placed above the short_code = generate_short_code() line, not below it.
Help me debug my URL shortener edge case tests.
Your URL shortener is production-ready. It validates input, avoids duplicates, and returns clear error messages. Next up, you'll take on an optional Secret Mission to add link expiration.
Secret mission
Add Link Expiration
Right now, every short link you create lives forever. In a real URL shortener, stale links pile up and can redirect to dead pages. In this Secret Mission, you will add configurable link expiration so short URLs automatically stop working after a set period.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. All three services used in this project fall under the AWS free tier, so there are no ongoing costs under normal usage.
Resources you used:
- DynamoDB table: url-shortener.
- Lambda function: url-shortener-function.
- API Gateway REST API: url-shortener-api.
- IAM execution role created by Lambda (with the AmazonDynamoDBFullAccess policy attached).
Keep everything running
No action needed. The free tier covers this project's usage. Lambda and API Gateway only charge when they receive requests. DynamoDB's always-free tier covers 25 GB of storage and 25 write/read capacity units.
You can keep using this as your personal URL shortener.
Pause - I'll come back to this later
Serverless resources like Lambda, API Gateway, and DynamoDB only incur costs when they are actively used. There is nothing to pause. If you are not sending requests, you are not being charged.
Your resources will stay as they are until you return.
Delete - I don't want to use this again
Remove all project resources to start fresh. Follow these steps in order.
- Delete the API Gateway REST API:
- Open the API Gateway console.
- Select your url-shortener-api API.
- In the Resources pane, choose API actions, then Delete API.
- In the dialog box, enter confirm and choose Delete.
- Delete the Lambda function:
- Open the Functions page in the Lambda console.
- Select url-shortener-function.
- Choose Actions, then Delete.
- Type confirm in the text input field and choose Delete.
- Delete the DynamoDB table:
- Open the DynamoDB console.
- In the left navigation pane, choose Tables.
- Select the checkbox beside the url-shortener table.
- Choose Delete and confirm the deletion.
- Delete the Lambda execution role:
- Open the Roles page of the IAM console.
- Select the execution role that was created for url-shortener-function.
- Choose Delete.
- In the Delete role dialog box, enter the role name and choose Delete.
Nice Work!
Nice Work!
Nice work! You just built a fully serverless URL shortener on AWS from scratch. Paste in a long URL, get back a short code, and anyone who visits that code gets redirected to the original destination.
You've learned how to:
- Build a serverless URL shortener using three core AWS services: Lambda for compute, API Gateway for a public HTTP endpoint, and DynamoDB for storing URL mappings.
- Write a Lambda function in Python that handles both creating short URLs (POST) and redirecting visitors to original URLs (GET) through a single function with Lambda proxy integration.
- Design a production-ready API with input validation, duplicate detection powered by a DynamoDB Global Secondary Index, and proper error handling for missing and malformed requests.
- Secret Mission: Implement link expiration using DynamoDB TTL and conditional redirect logic so short URLs automatically stop working after a configurable time period.
Ready to quiz yourself?