Serverless URL Shortener with Terraform

Build a serverless URL shortener API on AWS using Terraform as IaC.

Introduction

30 Second Summary

Every time you click a shortened link, a small service is mapping that short code to a full URL behind the scenes. These services run on the same serverless building blocks that power modern cloud infrastructure.

In this project, you will build a serverless URL shortener API on AWS, defining the entire backend as Terraform configuration files that you can deploy and destroy with a single command.

What You'll Build

A serverless URL shortener API that accepts long URLs via a POST request, generates short codes, and redirects visitors to the original URL when they visit with that code.

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

  • A DynamoDB table storing URL mappings as key-value pairs.
  • A Python Lambda function handling short URL creation and redirect logic.
  • An API Gateway v2 HTTP API exposing two public endpoints you can call from anywhere.
  • Secret Mission: Add automatic link expiration so short links self-destruct after 7 days.

Are there any prerequisites?

You need an AWS account (free tier eligible) and basic familiarity with HCL syntax and the Terraform workflow (init, plan, apply, destroy). No prior serverless experience is required.

Install Terraform and AWS CLI

Your Terraform CLI and AWS CLI are installed and configured. Now it is time to use them to define your URL shortener's infrastructure as code.

Every AWS resource your URL shortener needs will be declared in .tf files. In this step, you will create the project directory, configure the Terraform providers, and define the foundational resources: a DynamoDB table for storing URL mappings and an IAM role that your Lambda function will assume.

In this step, get ready to:
  • Create the project directory and Terraform provider configuration.
  • Define a DynamoDB table for URL mappings.
  • Create an IAM execution role with the permissions Lambda needs.
Create the project directory and provider configuration

Terraform needs a dedicated directory containing your configuration files. You will also declare which providers (AWS and archive) Terraform should download.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir url-shortener
cd url-shortener
  • Confirm the folder exists by running:
ls

You should see the empty url-shortener directory listed on your Desktop.

  • Create the variables file by running:
New-Item variables.tf
  • Open variables.tf in your preferred text editor and paste the following code:
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

What does this code do?

This declares an input variable called aws_region with a default value of us-east-1. Terraform uses variables to keep configuration flexible. You could override this value later without editing the main config.

  • Save variables.tf.
  • Create the main configuration file by running:
New-Item main.tf
  • Open main.tf and paste the following code:
terraform {
  required_version = ">= 1.15"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

What does this code do?

The terraform block pins the minimum Terraform version to 1.15 and declares two required providers: aws (for all AWS resources) and archive (for zipping the Lambda code later).

The provider "aws" block tells Terraform to deploy resources in the region defined by your aws_region variable.

  • Save main.tf.
  • Initialize Terraform to download the declared providers by running:
terraform init

You should see Terraform has been successfully initialized! in the output.

Seeing an error during init?

  • Check that your required_version and required_providers blocks are inside the terraform { } block, not outside it.
  • Make sure you saved both variables.tf and main.tf before running terraform init.
  • Confirm you are inside the url-shortener directory by running pwd.

Help me debug a terraform init error.

Define the DynamoDB table

Your URL shortener needs a place to store the mapping between short codes and original URLs. A DynamoDB table with on-demand billing is perfect for this because you only pay per request.

  • In main.tf, add the following resource block below the provider "aws" block:
resource "aws_dynamodb_table" "urls" {
  name         = "url-shortener"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "short_code"

  attribute {
    name = "short_code"
    type = "S"
  }
}

What does this code do?

This creates a DynamoDB table named url-shortener with a partition key called short_code (type S for string). Each item in the table will be one short URL mapping.

The PAY_PER_REQUEST billing mode means you are not pre-provisioning capacity. DynamoDB scales automatically and the free tier covers 25 read/write capacity units.

  • Save main.tf.

You will not see a change yet. The next substep adds the IAM role, then you will run terraform plan to verify everything together.

Create the IAM execution role and policies

Your Lambda function needs permission to write logs and read/write the DynamoDB table. In AWS, you grant these permissions by creating an IAM role that the Lambda service can assume.

  • In main.tf, add the following three resource blocks below the aws_dynamodb_table resource:
resource "aws_iam_role" "lambda_exec" {
  name = "url-shortener-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "lambda_dynamodb" {
  name = "url-shortener-dynamodb-access"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "dynamodb:PutItem",
        "dynamodb:GetItem"
      ]
      Resource = aws_dynamodb_table.urls.arn
    }]
  })
}

What do these resources do?

  • aws_iam_role creates a role that only lambda.amazonaws.com can assume. This is the trust relationship.
  • aws_iam_role_policy_attachment attaches the AWS-managed AWSLambdaBasicExecutionRole policy, which grants permission to write logs to CloudWatch.
  • aws_iam_role_policy creates an inline policy granting dynamodb:PutItem and dynamodb:GetItem on your specific table. This is the minimum access the Lambda needs.
  • Save main.tf.
  • Preview the infrastructure Terraform will create by running:
terraform plan

At the bottom of the output, you should see Plan: 4 to add, 0 to change, 0 to destroy. confirming your 4 resources (DynamoDB table, IAM role, role policy attachment, and inline policy).

Plan shows errors instead of 4 resources?

  • If you see a syntax error, check that every opening brace { has a matching closing brace }. HCL is sensitive to mismatched brackets.
  • If you see a reference error mentioning aws_dynamodb_table.urls, confirm the DynamoDB resource block appears before the IAM inline policy that references it.
  • Make sure you ran terraform init earlier in this directory. The plan command requires initialized providers.

Help me fix my terraform plan errors.

✔️ Awesome, I've got everything!

Great. Double check you have saved both main.tf and variables.tf.

ⓧ I'd like to double check the full code

Here is the complete variables.tf:

variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
  default     = "us-east-1"
}

Here is the complete main.tf:

terraform {
  required_version = ">= 1.15"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_dynamodb_table" "urls" {
  name         = "url-shortener"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "short_code"

  attribute {
    name = "short_code"
    type = "S"
  }
}

resource "aws_iam_role" "lambda_exec" {
  name = "url-shortener-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "lambda_dynamodb" {
  name = "url-shortener-dynamodb-access"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Action = [
        "dynamodb:PutItem",
        "dynamodb:GetItem"
      ]
      Resource = aws_dynamodb_table.urls.arn
    }]
  })
}

Your infrastructure foundation is defined. Next up, you will write the Python Lambda function that creates short URLs and handles redirects.

Define the Infrastructure Foundation

Your DynamoDB table and IAM role are ready. Now you need the compute layer that actually runs your URL shortener logic.

In this step, you will write the Python function that handles creating and redirecting short URLs. Then you will configure Terraform to package that code and deploy it as an AWS Lambda function.

In this step, get ready to:
  • Write the Python Lambda handler with create and redirect endpoints.
  • Configure Terraform to zip and deploy the Lambda function.
  • Verify the plan shows the new Lambda resources.
Write the Lambda handler function

The Lambda function needs to handle two types of requests. A POST to /shorten creates a new short URL. A GET to /{code} redirects the visitor to the original URL.

  • Create a lambda folder inside your url-shortener directory by running this command:
mkdir lambda
  • Create a new file called handler.py inside the lambda folder. Open it in your editor.
  • Add the imports, DynamoDB setup, utility function, and main router by pasting the following code into handler.py:
import json
import os
import string
import random
import boto3

# Connect to the DynamoDB table using the name from environment variables
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


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


def lambda_handler(event, context):
    # Route the request based on HTTP method and path
    http_method = event.get("requestContext", {}).get("http", {}).get("method", "")
    path = event.get("rawPath", "")

    if http_method == "POST" and path == "/shorten":
        return handle_shorten(event)
    elif http_method == "GET" and path != "/shorten":
        return handle_redirect(event)
    else:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid request"})
        }

What does this code do?

The boto3 library connects to AWS services from Python. The TABLE_NAME environment variable tells the function which DynamoDB table to use. Terraform will set this variable when it deploys the function.

The lambda_handler function is the entry point that AWS Lambda calls. It reads the HTTP method and path from the API Gateway event, then routes to the appropriate handler function.

  • Add the two endpoint handler functions below the code you just pasted:

def handle_shorten(event):
    # Parse the JSON body from the request
    try:
        body = json.loads(event.get("body", "{}"))
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid JSON body"})
        }

    original_url = body.get("url")
    if not original_url:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing 'url' field"})
        }

    # Generate a unique short code and store the mapping
    short_code = generate_short_code()

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

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


def handle_redirect(event):
    # Extract the short code from the URL path
    path_params = event.get("pathParameters", {})
    short_code = path_params.get("code", "")

    if not short_code:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing short code"})
        }

    # Look up the original URL in DynamoDB
    response = table.get_item(Key={"short_code": short_code})
    item = response.get("Item")

    if not item:
        return {
            "statusCode": 404,
            "body": json.dumps({"error": "Short URL not found"})
        }

    # Return a 301 redirect to the original URL
    return {
        "statusCode": 301,
        "headers": {"Location": item["original_url"]},
        "body": ""
    }

What does this code do?

The handle_shorten function parses the incoming JSON body, generates a random 6-character code, stores both the code and original URL in DynamoDB, and returns the short code to the caller.

The handle_redirect function extracts the short code from the URL path, looks it up in DynamoDB, and returns a 301 redirect header pointing to the original URL. If the code does not exist, it returns a 404 error.

  • Save lambda/handler.py.

Seeing a syntax error?

Make sure both handle_shorten and handle_redirect are at the same indentation level as lambda_handler. Python is sensitive to whitespace. Check for any extra spaces or tabs that might have crept in when pasting.

help me fix a syntax error in my handler.py Lambda function

✔️ Awesome, I've got everything!

Great. Double check you have saved lambda/handler.py before moving on.

ⓧ I'd like to double check the full code

import json
import os
import string
import random
import boto3

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


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


def lambda_handler(event, context):
    http_method = event.get("requestContext", {}).get("http", {}).get("method", "")
    path = event.get("rawPath", "")

    if http_method == "POST" and path == "/shorten":
        return handle_shorten(event)
    elif http_method == "GET" and path != "/shorten":
        return handle_redirect(event)
    else:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid request"})
        }


def handle_shorten(event):
    try:
        body = json.loads(event.get("body", "{}"))
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Invalid JSON body"})
        }

    original_url = body.get("url")
    if not original_url:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing 'url' field"})
        }

    short_code = generate_short_code()

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

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


def handle_redirect(event):
    path_params = event.get("pathParameters", {})
    short_code = path_params.get("code", "")

    if not short_code:
        return {
            "statusCode": 400,
            "body": json.dumps({"error": "Missing short code"})
        }

    response = table.get_item(Key={"short_code": short_code})
    item = response.get("Item")

    if not item:
        return {
            "statusCode": 404,
            "body": json.dumps({"error": "Short URL not found"})
        }

    return {
        "statusCode": 301,
        "headers": {"Location": item["original_url"]},
        "body": ""
    }
Configure Terraform to package and deploy the Lambda

Terraform needs to zip your Python code and upload it to AWS. The archive_file data source handles the zipping, and the aws_lambda_function resource handles the deployment.

  • Create a new file called lambda.tf in your url-shortener directory and paste the following code:
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda"
  output_path = "${path.module}/lambda.zip"
}

resource "aws_lambda_function" "url_shortener" {
  function_name    = "url-shortener"
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  runtime          = "python3.13"
  handler          = "handler.lambda_handler"
  role             = aws_iam_role.lambda_exec.arn

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.urls.name
    }
  }
}

resource "aws_cloudwatch_log_group" "lambda_logs" {
  name              = "/aws/lambda/${aws_lambda_function.url_shortener.function_name}"
  retention_in_days = 14
}

What does this configuration do?

The archive_file data source zips everything in the lambda/ directory into lambda.zip. The source_code_hash attribute tells Lambda to redeploy whenever the code changes.

The aws_lambda_function resource creates the Lambda function with the python3.13 runtime. It references the IAM role from main.tf and passes the DynamoDB table name as an environment variable.

The aws_cloudwatch_log_group captures Lambda execution logs with a 14-day retention period.

  • Save lambda.tf.

Seeing a reference error?

If Terraform complains about aws_iam_role.lambda_exec or aws_dynamodb_table.urls not being found, make sure main.tf is saved and contains those resource blocks. Terraform reads all .tf files in the same directory as a single configuration.

help me fix a Terraform reference error in my lambda.tf file

✔️ Awesome, I've got everything!

Great. Double check you have saved lambda.tf before moving on.

ⓧ I'd like to double check the full code

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda"
  output_path = "${path.module}/lambda.zip"
}

resource "aws_lambda_function" "url_shortener" {
  function_name    = "url-shortener"
  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  runtime          = "python3.13"
  handler          = "handler.lambda_handler"
  role             = aws_iam_role.lambda_exec.arn

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.urls.name
    }
  }
}

resource "aws_cloudwatch_log_group" "lambda_logs" {
  name              = "/aws/lambda/${aws_lambda_function.url_shortener.function_name}"
  retention_in_days = 14
}
Verify the Lambda plan

Time to confirm Terraform can read both files and understands the full configuration.

  • Run the plan command in PowerShell:
terraform plan
  • Look at the plan summary at the bottom. You should see a line showing resources to add, including the aws_lambda_function.url_shortener and aws_cloudwatch_log_group.lambda_logs resources marked with + create.
  • Confirm that the Lambda function resource shows runtime = "python3.13" in the plan output.

What should I see?

The plan output lists each resource Terraform will create, prefixed with a + symbol. You should see the Lambda function with its function_name, runtime, and handler attributes listed. The CloudWatch log group appears as a separate resource.

The final summary line shows the total count of resources to add, change, and destroy.

Plan failing with an error?

If you see an error about the lambda/ directory not existing, make sure you created the folder and handler.py is saved inside it. The archive_file data source needs the source directory to exist at plan time.

help me fix a terraform plan error for my Lambda function configuration

Your Lambda function is configured and ready to deploy. Next up, you will wire it to API Gateway so the outside world can reach your URL shortener.

Write and Package the Lambda Function

Your Lambda function and DynamoDB table are defined in Terraform, but they have no way to receive requests from the internet.

In this step, you will create an API Gateway HTTP API that acts as a public front door for your Lambda function. You will define routes, connect them to the function, and deploy the entire stack to AWS with a single command.

In this step, get ready to:
  • Create an API Gateway v2 HTTP API with a Lambda integration.
  • Define two public routes (POST and GET) that trigger the Lambda function.
  • Deploy the full infrastructure stack and get a live API endpoint.
Create the API Gateway HTTP API

API Gateway v2 HTTP APIs are lightweight, low-latency endpoints that forward incoming requests to backend services like Lambda. You need three resources to set up the base: the API itself, a stage for deployment, and an integration that connects the API to your function.

  • Create a new file called api_gateway.tf inside your url-shortener folder.
  • Add the following code to define the HTTP API, its deployment stage, and the Lambda integration:
resource "aws_apigatewayv2_api" "api" {
  name          = "url-shortener-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.api.id
  name        = "$default"
  auto_deploy = true
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id             = aws_apigatewayv2_api.api.id
  integration_type   = "AWS_PROXY"
  integration_uri    = aws_lambda_function.url_shortener.invoke_arn
  integration_method = "POST"
}

What does this code do?

The aws_apigatewayv2_api resource creates an HTTP API named url-shortener-api. The protocol_type is set to HTTP (as opposed to WebSocket).

The stage named $default keeps your endpoint URL clean by not adding a path prefix. Setting auto_deploy = true means any route changes are published automatically without a separate deploy step.

The integration connects the API to your Lambda function using AWS_PROXY mode. This passes the full HTTP request to Lambda and lets your function control the entire response.

  • Save api_gateway.tf.

Seeing a reference error?

If your editor highlights aws_lambda_function.url_shortener as unknown, make sure lambda.tf is saved in the same url-shortener folder. Terraform reads all .tf files in the directory together.

Help me fix a reference error in my api_gateway.tf file.

Add routes and Lambda permission

Routes tell API Gateway which HTTP method and path to listen for. You also need to explicitly grant API Gateway permission to invoke your Lambda function.

  • Add the following code below the integration resource in api_gateway.tf:
resource "aws_apigatewayv2_route" "post_shorten" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "POST /shorten"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_apigatewayv2_route" "get_redirect" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "GET /{code}"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.url_shortener.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

What does this code do?

The two route resources map incoming requests to your Lambda integration. POST /shorten handles URL creation. GET /{code} handles redirects, where {code} is a path parameter passed to your handler.

The aws_lambda_permission resource grants API Gateway the right to invoke your function. Without it, API Gateway gets an access denied error even though the route exists. The /*/* wildcard in the source ARN allows any method on any route of this API to trigger the function.

  • Save api_gateway.tf.

Worried about the permission scope?

The /*/* pattern is standard for Lambda proxy integrations. The first wildcard matches any HTTP method. The second matches any route path. This is safe because the permission only applies to this specific API, not to all APIs in your account.

Help me understand the Lambda permission source_arn pattern.

Deploy the entire stack

Before deploying, you need an output that prints the live API URL to your terminal. Then you will run terraform apply to provision every resource at once.

  • Create a new file called outputs.tf in the url-shortener folder and add this code:
output "api_endpoint" {
  description = "Base URL for the URL shortener API"
  value       = aws_apigatewayv2_api.api.api_endpoint
}

What does this output do?

Terraform outputs print values to your terminal after a successful apply. This one grabs the api_endpoint attribute from your API Gateway resource, which is the base URL you will use to make requests.

  • Save outputs.tf.
  • Deploy all resources to AWS by running this command in PowerShell:
terraform apply
  • Terraform will show a plan of all resources to create. Type yes and press Enter to confirm.

Wait for Terraform to finish creating all resources. At the end of the output, you should see a line like:

api_endpoint = "https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com"

Copy this URL. This is your live URL shortener API endpoint.

Seeing an error during apply?

If you see an IAM or permission error, your AWS credentials may have expired. Run aws sts get-caller-identity to confirm your credentials are still valid.

If Terraform reports a naming conflict, another resource with the same name may already exist in your AWS account. Check the AWS Console for duplicate resources.

Help me debug a terraform apply failure for my URL shortener.

✔️ Awesome, I've got everything!

Great. Double check you have saved both api_gateway.tf and outputs.tf.

ⓧ I'd like to double check the full code

Here is the complete api_gateway.tf file:

resource "aws_apigatewayv2_api" "api" {
  name          = "url-shortener-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.api.id
  name        = "$default"
  auto_deploy = true
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id             = aws_apigatewayv2_api.api.id
  integration_type   = "AWS_PROXY"
  integration_uri    = aws_lambda_function.url_shortener.invoke_arn
  integration_method = "POST"
}

resource "aws_apigatewayv2_route" "post_shorten" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "POST /shorten"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_apigatewayv2_route" "get_redirect" {
  api_id    = aws_apigatewayv2_api.api.id
  route_key = "GET /{code}"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.url_shortener.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

Here is the complete outputs.tf file:

output "api_endpoint" {
  description = "Base URL for the URL shortener API"
  value       = aws_apigatewayv2_api.api.api_endpoint
}

Your entire serverless stack is now live on AWS. Next up, you will send real requests to your API and watch short URLs get created and redirected.

Wire Up API Gateway and Deploy

Your entire infrastructure is live on AWS. The API endpoint that Terraform output is a real, publicly accessible URL backed by Lambda, API Gateway, and DynamoDB.

Now it is time to prove it works. You will send HTTP requests to both endpoints and verify data flows through the entire system.

In this step, get ready to:
  • Create a short URL by sending a POST request to your live API.
  • Test the redirect by visiting the short URL in your browser.
  • Verify the stored data directly in DynamoDB.
Create a short URL with a POST request

The /shorten endpoint accepts a JSON body with a url field and returns a random 6-character short code. You will call it from PowerShell using Invoke-RestMethod.

  • Store your API endpoint in a PowerShell variable by running this command (replace the URL with the one Terraform output in the previous step):
$API_ENDPOINT = "[[API_URL="https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com"]]"
  • Send a POST request to create a short URL by running this command:
Invoke-RestMethod -Method POST -Uri "$API_ENDPOINT/shorten" -Body '{"url":"https://developer.hashicorp.com/terraform"}' -ContentType "application/json"

What does this command do?

Invoke-RestMethod is PowerShell's built-in way to call HTTP APIs. The -Method POST flag specifies the HTTP method, -Body carries the JSON payload, and -ContentType tells the server to expect JSON.

Your Lambda function receives this request, generates a random 6-character code, stores the mapping in DynamoDB, and returns the code in the response.

You should see a response like this, with a random 6-character short_code value:

{
  "short_code": "aB3xYz",
  "original_url": "https://developer.hashicorp.com/terraform"
}
  • Copy the short_code value from your response. You will use it in the next substep.

Seeing an error instead of a short code?

  • Make sure you replaced the placeholder URL in the $API_ENDPOINT variable with your actual Terraform output URL.
  • Check that your URL does not have a trailing slash before /shorten. The full path should look like https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/shorten.
  • If you see a 500 error, your Lambda function may not have permission to write to DynamoDB. Run terraform apply again to ensure all IAM policies are in place.

Still stuck? Help me debug my POST request to the URL shortener API

Test the redirect in your browser

The GET endpoint takes a short code in the URL path and returns a 301 redirect to the original URL. Your browser follows the redirect automatically.

  • Open your web browser.
  • Paste the following URL into your address bar, replacing the placeholder values with your actual API endpoint and the short code from the previous substep:
[[API_URL="https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com"]]/[[SHORT_CODE="your-short-code"]]

Your browser should redirect you to the HashiCorp Terraform documentation page at https://developer.hashicorp.com/terraform.

What is happening behind the scenes?

When your browser sends a GET request to /{code}, API Gateway routes it to your Lambda function. The function looks up the short code in DynamoDB, finds the original URL, and returns a 301 redirect header.

Your browser sees the 301 status and the Location header, then automatically navigates to the original URL. This all happens in milliseconds.

Not redirecting to the Terraform docs page?

  • Double-check that the short code in your URL matches exactly what the POST response returned. Codes are case-sensitive.
  • Make sure there is no trailing slash after the short code.
  • If you see a 404 error, the code was not found in DynamoDB. Try creating a new short URL with another POST request.

Need more help? Help me debug my URL shortener redirect

Verify the data in DynamoDB

The final proof that everything works end-to-end is checking the actual data stored in your DynamoDB table. You can scan the table directly from the command line.

  • Return to PowerShell and scan your DynamoDB table by running this command:
aws dynamodb scan --table-name url-shortener --region us-east-1

What does this command do?

The aws dynamodb scan command reads every item in the specified table. For a small test table like this, a scan is fine. In production, you would use queries with specific key conditions instead.

The output confirms that your Lambda function successfully wrote the mapping between your short code and original URL to DynamoDB.

You should see output containing your short code and original URL:

{
    "Items": [
        {
            "short_code": {
                "S": "aB3xYz"
            },
            "original_url": {
                "S": "https://developer.hashicorp.com/terraform"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1
}

Scan returns empty items?

  • Confirm you are scanning the correct table name (url-shortener) and region (us-east-1).
  • If the POST request returned a short code but the table is empty, check CloudWatch logs for your Lambda function to see if DynamoDB writes are failing.

Still stuck? Help me figure out why my DynamoDB table is empty

Your URL shortener is fully operational. You have a live public API that creates short links and redirects visitors, all defined as Terraform code that you can destroy and recreate with a single command.

Test Your Live URL Shortener

Your infrastructure is deployed and your API endpoint is live. Now it's time to prove everything actually works end to end.

You'll send a real HTTP request to create a short URL, follow that short URL in your browser to confirm the redirect, then peek inside DynamoDB to see the data your Lambda function stored.

In this step, get ready to:
  • Create a short URL by sending a POST request to your live API.
  • Test the redirect by visiting the short URL in your browser.
  • Verify the stored data in DynamoDB using the AWS CLI.
Create a short URL with a POST request

The /shorten endpoint accepts a JSON body containing a long URL and returns a random 6-character short code. You'll call it from PowerShell using Invoke-RestMethod.

  • Store your API endpoint URL in a PowerShell variable by running this command:
$API_ENDPOINT = "[[API_ENDPOINT=\"your api_endpoint output URL e.g. https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com\"]]"

Where do I find this URL?

This is the api_endpoint value that Terraform printed at the end of terraform apply in the previous step. If you need to see it again, run terraform output api_endpoint in your project directory.

  • Send a POST request to create a short URL by running this command:
Invoke-RestMethod -Method POST -Uri "$API_ENDPOINT/shorten" -Body '{"url":"https://developer.hashicorp.com/terraform"}' -ContentType "application/json"

What does this command do?

Invoke-RestMethod is PowerShell's built-in HTTP client. It sends a POST request to your /shorten endpoint with a JSON body containing the long URL you want to shorten.

Your Lambda function receives this request, generates a random 6-character code, stores the mapping in DynamoDB, and returns the code in the response.

You should see a JSON response containing a short_code and the original_url you submitted.

  • Copy the short_code value from the response. You'll need it for the next test.

Getting an error or empty response?

  • Double-check that your $API_ENDPOINT variable doesn't have a trailing slash. The URL should end with .amazonaws.com and nothing after.
  • Make sure you included the /shorten path in the URI. Without it, the API Gateway route won't match.
  • If you see a 500 error, the Lambda function may still be initializing. Wait 10 seconds and try again.

Help me debug my Invoke-RestMethod POST request to the URL shortener API

Test the redirect in your browser

The GET endpoint takes a short code in the URL path and returns a 301 redirect to the original URL. Your browser will follow that redirect automatically.

  • Open your web browser.
  • Paste your API endpoint URL into the address bar, followed by a / and the short code you copied. For example:
[[API_ENDPOINT="your api_endpoint output URL"]]/[[SHORT_CODE="the short_code value from the POST response"]]
  • Press Enter to navigate to the URL.

Your browser should redirect you to the HashiCorp Terraform documentation page at developer.hashicorp.com/terraform.

Not redirecting?

  • Check that the short code in your URL exactly matches the one from the POST response. The code is case-sensitive.
  • Make sure there's no extra space or character between the / and the short code.
  • If you see a 404 response, the code may not exist in DynamoDB. Try creating a new short URL and using the fresh code.

Help me figure out why my short URL redirect isn't working

Verify the data in DynamoDB

The final check confirms that your Lambda function successfully wrote data to the DynamoDB table. You'll scan the table to see the stored mapping.

  • View all items in your DynamoDB table by running this command:
aws dynamodb scan --table-name url-shortener --region us-east-1

You should see a JSON response containing your short code and the original URL stored as attributes in the Items array.

What does this output mean?

The aws dynamodb scan command retrieves every item in the table. Each item has a short_code (the hash key) and an original_url field.

The Count field tells you how many URLs have been shortened. If you sent multiple POST requests, you'll see multiple items here.

Seeing an empty Items array?

  • Confirm the table name is exactly url-shortener. A typo will scan a non-existent table.
  • Confirm the region flag is us-east-1 (or whichever region you deployed to). A wrong region will point to a different table.
  • If the POST request earlier returned an error, no item was stored. Go back and confirm the POST returns a successful response first.

Help me troubleshoot my DynamoDB scan results

Secret mission

Add Link Expiration with DynamoDB TTL

Your short links live forever right now. What if you could make them self-destruct after 7 days? In this mission, you'll add automatic link expiration using DynamoDB Time to Live and epoch timestamps.

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 resources in this project stay within the AWS free tier for normal testing usage. DynamoDB and Lambda are always free for up to 1M requests per month. API Gateway is free for the first 12 months of your AWS account.

Resources you used:

  • DynamoDB table url-shortener (stores URL mappings).
  • Lambda function url-shortener (handles create and redirect logic).
  • API Gateway v2 HTTP API url-shortener-api (exposes public endpoints).
  • IAM role url-shortener-lambda-role with attached policies.
  • CloudWatch Log Group /aws/lambda/url-shortener.
  • Local url-shortener project directory on your Desktop.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your URL shortener or want to build on top of it.

  • Your API stays live at no cost for light usage within AWS free tier limits.
  • DynamoDB and Lambda are always free for up to 1M requests per month.
  • API Gateway HTTP API is free for 1M calls per month for the first 12 months of your AWS account.
  • You can monitor usage in the AWS Console under each service's dashboard.

Pause - I'll come back to this later

Serverless resources only incur cost when they receive requests. There is no running process to shut down or pause.

  • Your Lambda function, DynamoDB table, and API Gateway all sit idle at zero cost when not receiving traffic.
  • If you want to prevent anyone from hitting the API while you are away, you can delete the API Gateway stage. This removes public access without destroying the other resources.
  • When you come back, run terraform apply in your url-shortener directory to recreate the stage and restore public access.

Delete - I don't want to use this again

Remove all AWS resources and local project files to start completely fresh.

  • Open PowerShell and navigate to your url-shortener directory on your Desktop.
  • Destroy all AWS resources by running:
cd ~/Desktop/url-shortener
terraform destroy
  • When prompted, type yes and press Enter to confirm.
  • Wait for Terraform to finish removing all resources. You will see a message confirming the number of resources destroyed.
  • Delete the local project directory by running:
cd ~/Desktop
Remove-Item -Recurse -Force url-shortener
  • Confirm the folder is gone by running:
ls ~/Desktop
  • You should no longer see url-shortener in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built and deployed a fully functional serverless URL shortener API on AWS, defined entirely as Terraform configuration files.

You've learned how to:

  • Wire together DynamoDB, Lambda, API Gateway v2, and IAM into a working serverless architecture using Terraform resource references.
  • Deploy and test a live public API endpoint that shortens URLs and redirects users with a 301 response.
  • Manage the complete Terraform workflow: init, plan, apply, and destroy to provision and tear down multi-service infrastructure with a single command.
  • Secret Mission: Added automatic link expiration using DynamoDB TTL and epoch timestamps so short links self-destruct after 7 days.

Ready to quiz yourself?