Deploy a Serverless REST API on AWS
Build and deploy a CRUD REST API using Lambda, API Gateway, and DynamoDB.
Introduction
30 Second Summary
Every app needs a backend to store and retrieve data. Traditionally, that meant managing servers, patching operating systems, and paying for idle compute around the clock.
In this project, you will build and deploy a serverless REST API on AWS that handles product inventory data. You will define your entire cloud infrastructure in a single YAML file and deploy it with one command.
What You'll Build
A serverless CRUD API backed by AWS Lambda, Amazon API Gateway, and Amazon DynamoDB, deployed entirely through Infrastructure as Code using AWS SAM.
By the end of this project, you'll have:
- A Python Lambda function that handles create, read, update, and delete operations against a DynamoDB table.
- An API Gateway HTTP API with routes for GET, PUT, and DELETE requests, accessible via a public URL.
- A SAM template that defines all your cloud resources as code, deployable with a single command.
- Secret Mission: Add a category search endpoint powered by a DynamoDB Global Secondary Index.
Are there any prerequisites?
You need an AWS account with free tier access (created after July 2025) and basic Python knowledge. This project uses always-free AWS services, so you will not be charged.
Install the AWS CLI
Every resource you deploy in this project goes through the AWS CLI. It is the command-line tool that lets you manage AWS services directly from your terminal.
In this step, you will install the CLI on your Mac, connect it to your AWS account with credentials, and confirm the connection works.
In this step, get ready to:
- Install AWS CLI v2 on macOS.
- Configure the CLI with your AWS credentials.
- Verify the installation and credentials.
Download and install AWS CLI v2
The AWS CLI is a standalone tool you install on your machine. AWS provides an official macOS installer package that handles everything for you.
- Press Cmd+Space (macOS) to open Spotlight search.
- Type Terminal and press Enter to open it.
- Download the AWS CLI installer package by running this command:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
What does this command do?
The curl command downloads a file from the internet. This fetches the official AWS CLI v2 installer package and saves it as AWSCLIV2.pkg in your current directory.
- Install the package by running this command:
sudo installer -pkg AWSCLIV2.pkg -target /
What does this command do?
The sudo prefix runs the command with administrator privileges. The installer tool is a built-in macOS utility that installs .pkg files.
Your terminal will prompt you for your Mac login password. Type it (the characters will not appear on screen) and press Enter.
- Verify the installation by running:
aws --version
Check the output that appears in your terminal.
✔️ I see aws-cli/2.x.x
You should see something like aws-cli/2.27.41 Python/3.11.6 Darwin/... in the output. The exact numbers may differ, but as long as it starts with aws-cli/2. you are good to go.
ⓧ I see aws-cli/1.x.x
You have an older version of the AWS CLI installed. The v1 version is outdated and missing features this project needs.
- Run the install commands above again. The v2 installer will replace v1.
- Close your terminal completely and reopen it.
- Run aws --version again. You should now see aws-cli/2. at the start.
ⓧ Command not found
The installer did not link the aws command to your terminal's PATH.
- Close your terminal completely and reopen it.
- Run aws --version again.
- If it still fails, check that /usr/local/bin is in your PATH by running echo $PATH. The installer places the aws binary at /usr/local/bin/aws.
Configure your AWS credentials
The CLI needs credentials to talk to your AWS account. You will create an access key in the AWS console, then feed it into the CLI.
- Sign in to the AWS Management Console at https://console.aws.amazon.com/iam/.
- In the left navigation pane, select Users.
- Select your user name to open the user details page.
- Select the Security credentials tab.
- In the Access keys section, choose Create access key.
- For the use case, choose Command Line Interface (CLI) and select Next.
- Skip the optional tag and select Create access key.
- Copy both the Access key ID and Secret access key and save them somewhere safe. You will not be able to see the secret key again after leaving this page.
What is an access key?
An access key is a pair of values (an ID and a secret) that proves your identity to AWS. Think of it as a username and password specifically for programmatic access.
The secret access key is shown only once at creation time. If you lose it, you will need to delete the key and create a new one.
- Back in your terminal, configure the CLI by running:
aws configure
The command will prompt you for four values. Enter them one at a time, pressing Enter after each:
- AWS Access Key ID: paste the Access Key ID you copied.
- AWS Secret Access Key: paste the Secret Access Key you copied.
- Default region name: enter us-east-1.
- Default output format: enter json.
Where does this get saved?
The aws configure command stores your credentials in ~/.aws/credentials and your region and output preferences in ~/.aws/config. Every AWS CLI command you run from now on will use these values automatically.
Don't have an IAM user yet?
If you only have a root account and no IAM user, you can create one in the IAM console. Select Users in the sidebar, then Create user. Give it AdministratorAccess permissions for this learning project. Then return to the steps above to create an access key for that user.
Help me create an IAM user with the right permissions
Verify the installation and credentials
Now that the CLI is installed and configured, run one final command to prove everything works end to end.
- Confirm the CLI can reach AWS by running:
aws sts get-caller-identity
Check the output that appears in your terminal.
✔️ I see my account details
You should see a JSON response containing your UserId, Account number, and Arn. This confirms your credentials are valid and the CLI can communicate with AWS.
ⓧ Unable to locate credentials
This means aws configure did not save your credentials correctly.
- Run aws configure again and re-enter your Access Key ID and Secret Access Key carefully.
- Make sure you are pasting the full values with no extra spaces.
- Run aws sts get-caller-identity again.
Help me fix the unable to locate credentials error
ⓧ InvalidClientTokenId error
This means the Access Key ID you entered is not valid.
- Go back to the IAM console and confirm the access key is active (not deleted or deactivated).
- Run aws configure again and re-paste your keys.
- If you lost your secret key, delete the access key in IAM and create a new one.
Help me fix the InvalidClientTokenId error
Your AWS CLI is installed and connected. Next up, you will install the AWS SAM CLI, which adds the power of Infrastructure as Code deployments on top of the CLI you just set up.
Install the AWS SAM CLI
Your AWS CLI is configured and ready to communicate with AWS. Now you need a second tool that sits on top of it.
AWS SAM (Serverless Application Model) is a framework that lets you define serverless infrastructure in short YAML templates. The SAM CLI reads those templates, transforms them into full CloudFormation stacks, and deploys everything for you.
In this step, get ready to:
- Install the AWS SAM CLI on your Mac.
- Verify the installation runs successfully.
- Understand how SAM transforms templates into CloudFormation stacks.
Install the SAM CLI
The SAM CLI is a standalone tool that extends the AWS CLI. It handles building, packaging, and deploying serverless applications from your terminal.
- Check if the SAM CLI is already on your machine by running this command:
sam --version
✔️ I see a version number
The SAM CLI is already installed. You are ready to move on to the next substep.
ⓧ Command not found
The SAM CLI is not installed yet. Pick one of the two methods below to install it.
Homebrew (recommended)
- Install the SAM CLI with Homebrew by running this command:
brew install aws-sam-cli
What does this command do?
Homebrew downloads the community-managed SAM CLI formula and places the sam binary on your PATH automatically.
- Verify the installation succeeded by running:
sam --version
You should see output like SAM CLI, version 1.159.0 (your version number may be higher).
Package installer (GUI)
- Open the official SAM CLI installation page in your browser:
open https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html
- Scroll to the macOS section.
- Download the correct .pkg file for your Mac:
- Apple Silicon (M1/M2/M3/M4): download aws-sam-cli-macos-arm64.pkg.
- Intel: download aws-sam-cli-macos-x86_64.pkg.
- Double-click the downloaded .pkg file to open the installer.
- Follow the prompts in the installer. Accept the defaults and enter your Mac password when prompted.
- Click Close when the installation completes.
What does the installer do?
The package installer places the SAM CLI in /usr/local/aws-sam-cli and creates a symlink at /usr/local/bin/sam so you can run sam from any terminal session.
- Verify the installation succeeded by running:
sam --version
You should see output like SAM CLI, version 1.159.0 (your version number may be higher).
sam command still not found after installing?
Close your terminal window completely and open a new one. The installer adds sam to your PATH, but existing terminal sessions do not pick up the change automatically.
If a new terminal still cannot find sam, verify the symlink exists by running this:
which sam
You should see /usr/local/bin/sam in the output. If not, the installer may not have completed successfully. Try reinstalling using the other method.
Help me troubleshoot why sam is not found on my PATH after installing the AWS SAM CLI.
Understand what SAM does
Now that the CLI is installed, take a moment to understand how it fits into your deployment workflow.
What is AWS SAM?
AWS SAM is an open-source framework for defining serverless infrastructure as code. You write a short YAML template (typically 30-50 lines) that describes your Lambda functions, API routes, and database tables.
When you deploy, SAM transforms that short template into a full CloudFormation template (often 200+ lines). CloudFormation then provisions every AWS resource for you. This means you get repeatable, version-controlled infrastructure from a single file.
The SAM CLI handles building your code, packaging dependencies, and running the guided deployment. You will use three commands throughout this project: sam init, sam build, and sam deploy.
Your tools are locked and loaded. Next up, you will create a SAM project and define the serverless resources for your API.
Create the SAM Project
You have both the AWS CLI and SAM CLI installed and configured. Now it is time to create the actual project that defines your serverless API.
AWS SAM uses a project directory containing a template.yaml file (your infrastructure definition) and your application code. You will scaffold this project using sam init, then replace the default template with a custom one tailored to your product inventory API.
In this step, get ready to:
- Initialize a SAM project with the Python 3.12 runtime.
- Explore the generated project structure.
- Replace the default template with your custom serverless API template.
Initialize a new SAM project
The sam init command scaffolds a new serverless project with the correct directory structure that SAM expects. You will run it with flags that skip the interactive prompts and create a Hello World starter project with Python.
- Make sure you are on your Desktop by running this command:
cd ~/Desktop
- Create your SAM project by running this command:
sam init --runtime python3.12 --name product-inventory-api --app-template hello-world --package-type Zip --no-interactive
What does this command do?
- --runtime python3.12 sets the Lambda function runtime to Python 3.12.
- --name product-inventory-api creates a project folder called product-inventory-api.
- --app-template hello-world uses the Hello World starter template as a starting point.
- --package-type Zip packages the Lambda code as a zip archive.
- --no-interactive skips the interactive prompts so the command runs cleanly.
- Move into your new project folder by running this command:
cd product-inventory-api
- Confirm the project was created by running:
ls
You should see files including template.yaml, a hello_world/ directory, README.md, and other starter files.
Command not found?
Make sure sam is on your PATH by running sam --version. If it returns a version number, try the sam init command again. If sam is not recognized, revisit the SAM CLI installation from the previous step.
help me troubleshoot why sam init is failing
Explore the generated project structure
The scaffold gave you a working starter project. Before you replace it, take a quick look at what SAM generated.
- View the default template by running:
cat template.yaml
What is template.yaml?
This file is the heart of every SAM project. It defines your cloud infrastructure in YAML.
When you deploy, SAM transforms this short template into a full CloudFormation stack that provisions all your resources automatically. The default template defines a single Lambda function with one API endpoint. You will replace it with a template that defines a full CRUD API backed by DynamoDB.
Replace the default template with your custom API template
The starter project has the right directory structure, but the template and code are for a basic Hello World app. You will now replace them with your custom serverless API configuration.
- Remove the default function code and create your own src/ directory by running these commands:
rm -rf hello_world tests
mkdir src
touch src/app.py
Why rename hello_world to src?
The default scaffold puts code in hello_world/. Your custom template sets CodeUri: src/ to point Lambda at a src/ directory instead. This is a cleaner, more standard structure for production projects.
- Create the src/requirements.txt file by running this command:
echo "boto3" > src/requirements.txt
What is requirements.txt for?
This file lists the Python packages your Lambda function depends on. The boto3 library is the AWS SDK for Python, which your function will use to communicate with DynamoDB.
Boto3 is already pre-installed in the Lambda Python runtime. Including it in requirements.txt ensures SAM can resolve dependencies correctly during local builds.
- Now open template.yaml in your text editor and replace its entire contents with the following:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Product Inventory API
Globals:
Function:
Timeout: 10
MemorySize: 128
Resources:
ProductsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python3.12
CodeUri: src/
Architectures:
- x86_64
Environment:
Variables:
TABLE_NAME: !Ref ProductsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ProductsTable
Events:
GetItems:
Type: HttpApi
Properties:
Path: /items
Method: get
GetItem:
Type: HttpApi
Properties:
Path: /items/{id}
Method: get
PutItem:
Type: HttpApi
Properties:
Path: /items
Method: put
DeleteItem:
Type: HttpApi
Properties:
Path: /items/{id}
Method: delete
ProductsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: products-inventory
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Outputs:
ApiUrl:
Description: URL of the API endpoint
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
What does this template define?
This template declares three main things:
- ProductsFunction is an AWS Lambda function running Python 3.12. Its Events section defines four HTTP routes that API Gateway will handle.
- ProductsTable is a DynamoDB table using on-demand billing (PAY_PER_REQUEST), which means you only pay for actual reads and writes. It stays comfortably within the always-free tier.
- DynamoDBCrudPolicy is a SAM policy template that grants your function least-privilege access to the DynamoDB table. SAM creates the IAM role automatically.
The Outputs section exposes the API URL after deployment so you can test it with curl.
- Save template.yaml.
- Verify your project structure by running:
ls src/
You should see app.py and requirements.txt listed inside the src/ directory.
Missing files or wrong directory?
Make sure you are inside the product-inventory-api folder. Run pwd to confirm your current directory is ~/Desktop/product-inventory-api. If you are somewhere else, run cd ~/Desktop/product-inventory-api to get back.
help me fix my SAM project directory structure
✔️ Awesome, I've got everything!
Great. Double check you have saved template.yaml and confirm src/requirements.txt contains boto3.
ⓧ I'd like to double check the full code
Here is the complete template.yaml file:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Product Inventory API
Globals:
Function:
Timeout: 10
MemorySize: 128
Resources:
ProductsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python3.12
CodeUri: src/
Architectures:
- x86_64
Environment:
Variables:
TABLE_NAME: !Ref ProductsTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ProductsTable
Events:
GetItems:
Type: HttpApi
Properties:
Path: /items
Method: get
GetItem:
Type: HttpApi
Properties:
Path: /items/{id}
Method: get
PutItem:
Type: HttpApi
Properties:
Path: /items
Method: put
DeleteItem:
Type: HttpApi
Properties:
Path: /items/{id}
Method: delete
ProductsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: products-inventory
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Outputs:
ApiUrl:
Description: URL of the API endpoint
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
And here is src/requirements.txt:
boto3
Your project structure is ready with a custom SAM template defining the full serverless stack. Next up, you will write the Python Lambda function that handles all four CRUD operations.
Write the Lambda Function
Your SAM template defines the infrastructure, but it points to a handler function that doesn't exist yet.
When API Gateway receives an HTTP request, it triggers your Lambda function with an event object containing the route, method, and body. Your function inspects that event and performs the matching database operation.
In this step, get ready to:
- Set up the DynamoDB connection and a JSON helper class.
- Write a Lambda handler that routes requests to CRUD operations.
Set up the module and DynamoDB connection
The Lambda function needs to connect to your DynamoDB table and handle a quirk of how DynamoDB stores numbers. DynamoDB returns numbers as Python Decimal objects, which the built-in JSON encoder can't serialize.
- Open src/app.py in your code editor and replace any placeholder content with the following:
import json
import os
import boto3
from decimal import Decimal
# Connect to DynamoDB using the table name from environment variables
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
# Custom JSON encoder that converts Decimal values to floats
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
What does this code do?
- boto3.resource("dynamodb") creates a high-level connection to the DynamoDB service using boto3's resource interface.
- os.environ["TABLE_NAME"] reads the table name that SAM injects at runtime (the !Ref ProductsTable you defined in template.yaml).
- DecimalEncoder converts DynamoDB's Decimal numbers into regular floats so json.dumps() can serialize them in API responses.
- Save src/app.py.
- You won't see a change yet. The handler function in the next section uses this setup code.
Write the request routing handler
API Gateway's HTTP API sends a routeKey field in the event object that combines the HTTP method and path (like GET /items or DELETE /items/{id}). Your handler uses this to decide which database operation to perform.
- Below the DecimalEncoder class in src/app.py, add the handler function:
def lambda_handler(event, context):
# Extract the route and path parameters from the API Gateway event
route_key = event.get("routeKey", "")
path_parameters = event.get("pathParameters") or {}
body = {}
status_code = 200
headers = {"Content-Type": "application/json"}
try:
if route_key == "GET /items":
# Return all items from the table
response = table.scan()
body = response.get("Items", [])
elif route_key == "GET /items/{id}":
# Return a single item by its ID
item_id = path_parameters["id"]
response = table.get_item(Key={"id": item_id})
body = response.get("Item", {})
if not body:
status_code = 404
body = {"message": "Item not found"}
elif route_key == "PUT /items":
# Create or update an item
request_body = json.loads(event.get("body", "{}"))
item = {
"id": request_body["id"],
"name": request_body["name"],
"price": Decimal(str(request_body["price"])),
"category": request_body.get("category", "general"),
}
table.put_item(Item=item)
body = {"message": f"Item {item['id']} created/updated"}
elif route_key == "DELETE /items/{id}":
# Delete an item by its ID
item_id = path_parameters["id"]
table.delete_item(Key={"id": item_id})
body = {"message": f"Item {item_id} deleted"}
else:
status_code = 400
body = {"message": f"Unsupported route: {route_key}"}
except KeyError as e:
status_code = 400
body = {"message": f"Missing required field: {str(e)}"}
except Exception as e:
status_code = 500
body = {"message": f"Internal server error: {str(e)}"}
return {
"statusCode": status_code,
"headers": headers,
"body": json.dumps(body, cls=DecimalEncoder),
}
What does this code do?
The handler routes each request to the correct DynamoDB operation based on the routeKey:
- GET /items calls table.scan() to return every item in the table.
- GET /items/{id} calls table.get_item() to fetch one item by its primary key.
- PUT /items calls table.put_item() to create or replace an item with the fields from the request body.
- DELETE /items/{id} calls table.delete_item() to remove an item by its primary key.
The try/except block catches missing fields (400 error) and unexpected failures (500 error) so your API always returns a clean JSON response.
Why use boto3's resource interface?
Boto3 offers two interfaces: a low-level client and a high-level resource.
The resource interface handles DynamoDB's type marshaling automatically. You write {"id": "abc"} instead of {"id": {"S": "abc"}}. This makes the code cleaner and more Pythonic.
- Save src/app.py.
- You'll deploy and test all four CRUD operations against a live endpoint in the next step.
Seeing red underlines in your editor?
- If your editor highlights boto3 as unresolved, that's expected. Boto3 is pre-installed in the Lambda Python runtime, so it runs fine once deployed even if your local editor can't find it.
- Check that your indentation uses consistent spaces (not a mix of tabs and spaces). Python is strict about this.
Help me fix a syntax or import error in my Lambda function.
✔️ Awesome, I've got everything!
Great. Double check you've saved src/app.py and you're ready to deploy.
ⓧ I'd like to double check the full code
Here's the complete src/app.py file. Make sure yours matches exactly:
import json
import os
import boto3
from decimal import Decimal
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def lambda_handler(event, context):
route_key = event.get("routeKey", "")
path_parameters = event.get("pathParameters") or {}
body = {}
status_code = 200
headers = {"Content-Type": "application/json"}
try:
if route_key == "GET /items":
response = table.scan()
body = response.get("Items", [])
elif route_key == "GET /items/{id}":
item_id = path_parameters["id"]
response = table.get_item(Key={"id": item_id})
body = response.get("Item", {})
if not body:
status_code = 404
body = {"message": "Item not found"}
elif route_key == "PUT /items":
request_body = json.loads(event.get("body", "{}"))
item = {
"id": request_body["id"],
"name": request_body["name"],
"price": Decimal(str(request_body["price"])),
"category": request_body.get("category", "general"),
}
table.put_item(Item=item)
body = {"message": f"Item {item['id']} created/updated"}
elif route_key == "DELETE /items/{id}":
item_id = path_parameters["id"]
table.delete_item(Key={"id": item_id})
body = {"message": f"Item {item_id} deleted"}
else:
status_code = 400
body = {"message": f"Unsupported route: {route_key}"}
except KeyError as e:
status_code = 400
body = {"message": f"Missing required field: {str(e)}"}
except Exception as e:
status_code = 500
body = {"message": f"Internal server error: {str(e)}"}
return {
"statusCode": status_code,
"headers": headers,
"body": json.dumps(body, cls=DecimalEncoder),
}
Your Lambda function is ready to handle requests. Next up, you'll deploy the entire stack to AWS and test every CRUD operation against your live API endpoint.
Deploy and Test Your API
Your Lambda function and SAM template are complete. The code exists on your machine, but it is not yet running on AWS.
In this step, you will build the project, deploy the entire stack to AWS with a single command, and then test your live API from the terminal using curl.
In this step, get ready to:
- Build and deploy the CloudFormation stack using SAM.
- Retrieve the API endpoint URL from the deployment outputs.
- Test all four CRUD operations against your live API with curl.
Build the project with sam build
Before deploying, SAM needs to gather your code and dependencies into a structured build folder. The sam build command reads your template.yaml, installs packages from requirements.txt, and organizes everything into a .aws-sam directory.
- Build the project by running this command in your project directory:
sam build
What does this command do?
The sam build command reads your template.yaml to find the CodeUri (your src/ folder), installs the Python dependencies listed in requirements.txt, and packages everything into the .aws-sam/build directory.
This build folder is what SAM uploads to AWS during deployment. You must run sam build after any change to your code or template before redeploying.
- You should see output ending with Build Succeeded and a note that your build artifacts are in .aws-sam/build.
Build not succeeding?
- Make sure you are running the command from your SAM project's root directory (the folder containing template.yaml).
- Confirm that your template.yaml has CodeUri: src/ and that the src/ folder contains app.py and requirements.txt.
Ask for help: Why is my sam build failing?
Deploy the stack with sam deploy --guided
Now you will push everything to AWS. The sam deploy --guided command walks you through an interactive flow that configures your stack name, region, and permissions.
It then transforms your SAM template into CloudFormation and creates all your resources in a single stack.
- Deploy the stack by running this command:
sam deploy --guided
What happens during guided deployment?
The --guided flag triggers an interactive flow that asks you to configure deployment settings. SAM uploads your code to an S3 bucket it manages, transforms your template into full CloudFormation, and creates a stack containing all your resources.
Your responses are saved to a samconfig.toml file. Future deployments can use sam deploy without the --guided flag because the settings are already stored.
The CLI will prompt you with several questions. Respond to each as follows:
- For Stack Name, enter products-api.
- For AWS Region, press Enter to accept your default region (the one you set during aws configure).
- For Confirm changes before deploy, enter y.
- For Allow SAM CLI IAM role creation, press Enter to accept the default (Y). This lets SAM create the IAM execution role your Lambda function needs.
- For Disable rollback, press Enter to accept the default (N).
- For the prompt asking whether functions without authorization are okay, enter y. This prompt appears multiple times (once per API route). Enter y each time.
- For Save arguments to configuration file, press Enter to accept the default (Y).
- For SAM configuration file and SAM configuration environment, press Enter to accept the defaults.
Why does SAM ask about authorization?
SAM detects that your API Gateway endpoint has no authentication configured. This means anyone with the URL can call your API. For a learning project this is fine, but in production you would add authentication.
The prompt appears once per API route because SAM checks each path individually. Since your template defines four routes, you will answer y multiple times.
After you answer the prompts, SAM shows you a changeset (the list of resources it plans to create). When prompted to deploy the changeset, enter y.
Deployment typically takes 1-2 minutes. You will see resource creation progress in your terminal.
- Wait for the deployment to complete. At the end of the output, look for the Outputs section containing your ApiUrl value.
- Copy the ApiUrl value from the outputs. It looks like https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com.
Deployment failed?
- If you answered N to the authorization prompt, SAM will refuse to proceed. Run sam deploy --guided again and enter y when asked about authorization.
- If you see a credentials error, confirm that aws sts get-caller-identity still works. If not, re-run aws configure with valid credentials.
- If CloudFormation rolls back, check the error message in the terminal output. A common cause is a naming conflict if you previously created a DynamoDB table named products-inventory.
Ask for help: Why did my SAM deployment fail?
Test the CRUD operations with curl
Your API is live. Time to test all four operations against it. You will create a product, list all products, retrieve one by ID, and then delete it.
- Create a product by running this curl command (replace the URL with your own ApiUrl):
curl -X PUT [[API_URL="your ApiUrl from the deployment output"]]/items \
-H "Content-Type: application/json" \
-d '{"id": "1", "name": "Widget", "price": 9.99, "category": "gadgets"}'
What does this request do?
This sends a PUT request to /items with a JSON body. Your Lambda function receives this, matches the PUT /items route, and writes the item to DynamoDB.
The -H flag sets the Content-Type header. The -d flag passes the JSON body containing your product data.
- You should see a response like:
{"message": "Item 1 created/updated"}
- List all products by running:
curl [[API_URL="your ApiUrl from the deployment output"]]/items
- You should see a JSON array containing the product you just created:
[{"id": "1", "name": "Widget", "price": 9.99, "category": "gadgets"}]
- Retrieve a single product by its ID:
curl [[API_URL="your ApiUrl from the deployment output"]]/items/1
- You should see the single product object returned:
{"id": "1", "name": "Widget", "price": 9.99, "category": "gadgets"}
- Delete the product by running:
curl -X DELETE [[API_URL="your ApiUrl from the deployment output"]]/items/1
- You should see:
{"message": "Item 1 deleted"}
- Confirm the deletion by listing all items again:
curl [[API_URL="your ApiUrl from the deployment output"]]/items
- You should see an empty array [], confirming the product was removed from DynamoDB.
Getting an error from curl?
- Make sure your URL does not have a trailing slash. The correct format is https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/items without a trailing slash after /items.
- If you see {"message":"Internal Server Error"}, the Lambda function may have hit an exception. Check that your template.yaml has the TABLE_NAME environment variable set to !Ref ProductsTable.
- If you get a DNS resolution error, double-check that you copied the full ApiUrl from your deployment outputs without any extra spaces or missing characters.
Ask for help: Why is my API returning an error?
Your serverless API is live and fully functional. Anyone with the URL can create, read, and delete products in your DynamoDB table. Next up, take on the Secret Mission to add a category search powered by a Global Secondary Index.
Secret mission
Add a Category Search with a Global Secondary Index
Your API can fetch all items or one item by ID. But what if a user wants to browse all products in a specific category? Right now, the only way to do that is to scan the entire table and filter client-side. In this mission, you'll add an efficient server-side search using a DynamoDB Global Secondary Index.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running or delete them entirely. Lambda and DynamoDB are always-free services, so your deployed API costs nothing while idle. API Gateway only charges per request, which is negligible for a demo project.
Resources you used:
- CloudFormation stack products-api (includes Lambda function, API Gateway HTTP API, DynamoDB table, and IAM role).
- Local SAM project files (template.yaml, src/app.py, src/requirements.txt, samconfig.toml).
Keep everything running
No action needed. Choose this if you are still actively experimenting with your API or plan to extend it further.
- Lambda and DynamoDB are always-free services. They do not consume credits regardless of whether they exist in your account.
- API Gateway only charges when requests hit your endpoint. For a demo project with occasional testing, this is effectively zero cost.
- Your local project files remain on disk. You can redeploy or modify at any time with sam build and sam deploy.
Pause - I'll come back to this later
There is nothing to pause. Serverless resources only incur costs when requests come in. Your stack sits idle at zero cost until you use it again.
- Your CloudFormation stack stays deployed but costs nothing while idle.
- Your local files remain on disk, ready for future edits and redeployments.
- When you come back, run sam build and sam deploy from your project folder to push any changes.
Delete - I don't want to use this again
Remove all project resources to keep your AWS account clean. This deletes the entire CloudFormation stack and all associated AWS resources.
- Navigate to your SAM project folder (the one containing template.yaml) in your terminal.
- Delete the CloudFormation stack and all AWS resources by running:
sam delete
- When prompted, confirm that you want to delete the stack products-api by entering y.
- When prompted to delete the template file in S3, enter y.
What does sam delete remove?
This single command deletes your Lambda function, API Gateway HTTP API, DynamoDB table, IAM role, and the S3 artifacts that SAM uploaded during deployment. Everything in the CloudFormation stack is removed.
- Delete your local project files by running:
cd ~/Desktop
rm -rf sam-app
Find your project folder name
If you named your project something other than sam-app during sam init, replace sam-app with whatever you named your project folder.
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see your project folder in the output.
That's a Wrap!
That's a Wrap!
Nice work. You just deployed a production-grade serverless REST API to AWS using Infrastructure as Code.
You've learned how to:
- Build a Python Lambda function that handles CRUD operations with DynamoDB using boto3.
- Define all infrastructure in an AWS SAM template and deploy the entire stack with a single sam deploy command.
- Create an API Gateway HTTP API with proper routing and test a live, publicly accessible endpoint using curl.
- Added efficient querying with a DynamoDB Global Secondary Index to search products by category.
Ready to quiz yourself?