Build a Serverless Task API on AWS
Build a serverless REST API with Lambda, DynamoDB, and API Gateway.
Introduction
30 Second Summary
Every app you use, from to-do lists to shopping carts, relies on an API to process your actions behind the scenes. Traditionally, those APIs run on servers that someone has to manage, patch, and pay for around the clock.
In this project, you will build a serverless Task Tracker API on AWS that handles creating, reading, and deleting tasks without managing a single server. Along the way, you will write plain-English analogies for 10 core AWS services, building a cheat sheet you can reference in interviews.
What You'll Build
You will paste your API's public URL into a browser, hit Enter, and see a live JSON response listing tasks from a cloud database you created with a single command.
By the end of this project, you'll have:
- A live serverless API you can call from any browser or terminal to create, read, and delete tasks in a DynamoDB database.
- Explain what Lambda, API Gateway, IAM, CloudWatch, and S3 do to a non-technical hiring manager using analogies you wrote yourself.
- Pull out a personal AWS Services Cheat Sheet with plain-English analogies for all 10 core services in your next interview.
- Secret Mission: Calculate what your API costs at scale and write a FinOps recommendation that proves you think about cloud spending.
Are there any prerequisites?
You need an AWS account. No other tools are required. This project walks you through installing the AWS CLI.
Before We Start
Before you touch any AWS resources, take a moment to lock in what you are about to build and why it matters for your career.
Install the AWS CLI and Set Up Your Workspace
Your serverless API starts with a single tool: the AWS CLI. Every database table, function, and API endpoint in this project gets created through commands you type in your terminal.
In this step, you will install the AWS CLI on Windows, connect it to your AWS account with your credentials, and set up a local project folder. You will also start a cheat sheet where you record plain-English analogies for each AWS service you encounter.
In this step, get ready to:
- Install the AWS CLI on Windows.
- Configure the CLI with your AWS credentials.
- Create a project folder and start your services cheat sheet.
Install the AWS CLI on Windows
The AWS CLI lets you control AWS services directly from your terminal. Instead of clicking through the AWS console for every action, you type a single command and get instant results.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Install the AWS CLI by running this command:
irm https://awscli.amazonaws.com/v2/install.ps1 | iex
What does this command do?
This command downloads the official AWS CLI install script from Amazon's servers and runs it automatically. It installs the CLI for your current user, so no administrator privileges are needed.
The installation may take a minute or two while it downloads and sets up the files.
Seeing a script execution error?
If PowerShell blocks the script due to your execution policy, run this alternative command instead:
powershell -ExecutionPolicy Bypass -Command "irm https://awscli.amazonaws.com/v2/install.ps1 | iex"
If this still does not work, download the MSI installer directly from https://awscli.amazonaws.com/AWSCLIV2.msi and double-click the file to install.
Help me install the AWS CLI on Windows
- Close your PowerShell window completely.
- Open a new PowerShell window by pressing the Windows key, typing PowerShell, and pressing Enter.
Why close and reopen?
The installer adds the aws command to your system path. PowerShell only reads the path when it first opens, so you need a fresh window to pick up the change.
- Confirm the installation by running this command:
aws --version
✔️ I see a version number
You should see output starting with aws-cli/2, for example aws-cli/2.27.41. The AWS CLI is installed and ready.
ⓧ Command not recognized
- Make sure you closed and reopened PowerShell after the installation finished.
- If you still see the error, try restarting your computer and opening a fresh PowerShell window.
- As a fallback, download the MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi, run it, restart PowerShell, and try again.
Help me get the aws command working in PowerShell
Configure the CLI with your AWS credentials
The CLI is installed, but it does not know which AWS account to connect to. You need to provide your IAM access keys so the CLI can make requests on your behalf.
Where to find your access keys
You need an Access Key ID and Secret Access Key to continue. If you do not have these yet, create them in the AWS console:
- Sign in to the IAM console.
- In the upper-right navigation bar, click your user name, then choose Security credentials.
- In the Access keys section, click Create access key.
- Select Command Line Interface (CLI), then click Next.
- Click Create access key and save both values somewhere safe. You will not be able to view the secret key again.
- Connect the CLI to your AWS account by running the command below. This starts an interactive session that asks four questions in your terminal.
aws configure
- At the AWS Access Key ID prompt, paste your access key ID.
- At the AWS Secret Access Key prompt, paste your secret access key.
- At the Default region name prompt, type us-east-1.
- At the Default output format prompt, type json.
Why us-east-1?
US East (N. Virginia) is the default AWS region and the one with the widest service availability. All the resources you create in this project will live in this region.
Before you run the next command, what do you think the CLI will return if your credentials are configured correctly? Think about what information AWS needs to identify you.
- Verify your connection by running this command:
aws sts get-caller-identity
You should see a JSON response containing your AWS account ID, user ID, and ARN. This confirms the CLI is connected to your account.
Not seeing your account ID?
- Double-check that you pasted the correct Access Key ID and Secret Access Key. They are case-sensitive and must not include extra spaces.
- Make sure you did not swap the two values. The Access Key ID starts with AKIA.
- Run aws configure again to re-enter your credentials.
Help me fix my AWS CLI credentials
Create your project folder and start your cheat sheet
You need a local folder to hold the code files you will write in later steps. You will also create a cheat sheet file where you record plain-English analogies for each AWS service as you use it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir task-tracker-api
cd task-tracker-api
- Confirm you are in the right folder by running:
pwd
You should see a path ending in task-tracker-api, confirming you are inside your new project folder.
- Create your cheat sheet file with the first two entries by running these commands:
Set-Content cheat-sheet.txt "AWS CLI: The remote control for your AWS account."
Add-Content cheat-sheet.txt "IAM: The security guard that checks IDs at every door."
What do these entries mean?
The AWS CLI entry captures what you just experienced. You used the CLI as a remote control to connect to your AWS account from your terminal.
The IAM entry captures what made that connection possible. The access keys you entered are IAM credentials. IAM controls who can access your AWS account and what they are allowed to do.
- Verify the file contents by running:
Get-Content cheat-sheet.txt
You should see both entries printed in your terminal.
✔️ Awesome, I've got everything!
Great. Double check you see both lines in the output, then move on.
ⓧ I'd like to double check the full code
Your cheat-sheet.txt file should contain exactly this:
AWS CLI: The remote control for your AWS account.
IAM: The security guard that checks IDs at every door.
If your file does not match, run the Set-Content and Add-Content commands again. Set-Content overwrites the file, so running both commands in order will fix any issues.
File not showing the right content?
- Make sure you are inside the task-tracker-api folder. Run pwd to check.
- If you accidentally ran Add-Content twice, the IAM line will appear twice. Run Set-Content first to reset the file, then Add-Content once.
Help me create my cheat sheet file in PowerShell
Your CLI is ready and your workspace is set up. Next up, you will create your first AWS resource: a DynamoDB database table that stores your tasks.
Create Your Database and See Serverless Storage in Action
Your CLI is connected and your workspace is ready. Now you need somewhere to actually store tasks before you can build an API around them.
Amazon DynamoDB is a serverless database. You create a table with a single command, and AWS handles all the server provisioning, scaling, and maintenance behind the scenes. This is the first moment you will see "serverless" in action.
In this step, get ready to:
- Create a DynamoDB table to store your tasks.
- Add and retrieve a test item to see how DynamoDB reads and writes data.
- Record cheat-sheet entries for DynamoDB and S3.
Create your DynamoDB table
Every task your API handles needs to live somewhere persistent. DynamoDB gives you a fully managed database table without provisioning a single server or guessing how much storage you need.
- Create your task-tracker table by running this command in PowerShell:
aws dynamodb create-table --table-name task-tracker --attribute-definitions AttributeName=taskId,AttributeType=S --key-schema AttributeName=taskId,KeyType=HASH --billing-mode PAY_PER_REQUEST
What does this command do?
- --table-name task-tracker names your table. Every DynamoDB operation targets this name.
- --attribute-definitions and --key-schema define the primary key. Here, taskId is a string (S) that uniquely identifies each task.
- --billing-mode PAY_PER_REQUEST means you pay only when you read or write data. No traffic, no charge.
You should see JSON output containing a TableDescription with TableStatus set to CREATING. The table is being provisioned.
Seeing an error?
- If you see a credentials error, run aws sts get-caller-identity to confirm your CLI is still connected to your AWS account.
- If the table already exists, you may have run this command before. You can continue to the next command.
- help me debug my DynamoDB create-table error
- Wait for the table to become active by running this command:
aws dynamodb wait table-exists --table-name task-tracker
This command waits silently, polling AWS every 20 seconds until the table status is ACTIVE. When your cursor returns with no output, the table is ready.
- Verify the table exists by running a scan:
aws dynamodb scan --table-name task-tracker
You should see an empty Items list with Count at 0. Your table is live and ready for data.
What is DynamoDB?
DynamoDB is a fully managed NoSQL database. Unlike traditional databases where you spin up a server, install software, and plan capacity, DynamoDB is ready the moment you create a table.
It scales up and down automatically based on traffic. You never worry about storage limits, backups, or the database crashing under load.
Add a test item to your table
DynamoDB stores data as items, which are collections of key-value attributes. Each item must include the primary key you defined (taskId), and can contain any additional attributes you want.
- Add a test task to your table by running this command:
aws dynamodb put-item --table-name task-tracker --item '{"taskId":{"S":"test-001"},"title":{"S":"Learn AWS"},"status":{"S":"in-progress"}}'
What is this JSON format?
DynamoDB requires each attribute value to include a type descriptor. {"S":"test-001"} means "this is a String with the value test-001."
Your item has three attributes: taskId (the primary key), title, and status. DynamoDB did not require you to define title or status up front. You can add any attributes to any item at any time.
Before you run the next command, what do you expect to see when you retrieve the item you just added?
- Retrieve your test item to confirm it was saved by running:
aws dynamodb get-item --table-name task-tracker --key '{"taskId":{"S":"test-001"}}'
You should see your item returned with all three attributes: taskId, title, and status. You wrote data to a cloud database and read it back, all from your terminal.
Not seeing your item?
- Make sure you wrapped the JSON in single quotes, not double quotes. PowerShell treats single-quoted strings literally, which preserves the double quotes inside the JSON.
- Check that taskId in your get-item key matches the taskId you used in put-item. The values are case-sensitive.
- help me debug my DynamoDB put-item or get-item error
Record your cheat-sheet entries for DynamoDB and S3
You have seen DynamoDB in action. Now capture that understanding in your cheat sheet while it is fresh. You will also add an entry for S3 now, because you will use it in the next step to store your Lambda code package.
- Open cheat-sheet.txt in your project folder on the Desktop.
- Add these two new entries below the existing IAM line:
DynamoDB: A giant spreadsheet in the cloud that grows and shrinks automatically. You never worry about running out of rows or the spreadsheet crashing.
S3: A limitless file cabinet in the cloud. You can store anything from photos to code packages, and it never runs out of space.
- Save cheat-sheet.txt.
- Confirm your entries by running this command from your project folder:
type cheat-sheet.txt
You should see all four entries: AWS CLI, IAM, DynamoDB, and S3.
✔️ Awesome, I've got everything!
Great. Make sure you have saved cheat-sheet.txt.
ⓧ I'd like to double check the full file
Your complete cheat-sheet.txt should look exactly like this:
AWS CLI: The remote control for your AWS account.
IAM: The security guard that checks IDs at every door.
DynamoDB: A giant spreadsheet in the cloud that grows and shrinks automatically. You never worry about running out of rows or the spreadsheet crashing.
S3: A limitless file cabinet in the cloud. You can store anything from photos to code packages, and it never runs out of space.
Why write the S3 entry now?
In the next step, you will upload your Lambda function code to an S3 bucket. Writing the analogy now gives you context before you use the service, so the command makes sense when you run it.
You created a serverless database, wrote data to it, and read it back. No servers to configure, no capacity to plan. Your task-tracker table is ready and waiting for tasks. Next up, you will write a Lambda function that talks to this table and handles the create, read, and delete logic for your API.
Write and Deploy Your Lambda Function
In the last step, you created your database and proved it works by storing and retrieving a test task. Your DynamoDB table is ready and waiting.
The problem is, nothing can talk to that table yet. You need code that listens for requests and translates them into database operations. Lambda lets you run that code without setting up or managing a server. You write the logic, upload it, and AWS handles everything else. But before Lambda can touch DynamoDB or write logs, you need to create an IAM role that grants those specific permissions.
In this step, get ready to:
- Create an IAM execution role that gives Lambda permission to access DynamoDB and write logs.
- Write the Lambda function code that handles creating, reading, and deleting tasks.
- Deploy your function to AWS and test it with a live invocation.
Create the IAM execution role for Lambda
Every Lambda function needs an execution role. This role tells AWS what services the function is allowed to use. Without it, your function cannot read from DynamoDB or write logs to CloudWatch.
The first step is creating a trust policy file. This file tells AWS that the Lambda service is allowed to assume this role.
- Create the trust policy file by running this command in your project folder:
@'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
'@ | Out-File -Encoding utf8 trust-policy.json
What does this file do?
The trust policy is like an ID badge. It tells AWS that only the Lambda service is allowed to "wear" this role. Without it, the role cannot be used by any service.
- Confirm the file was created by running:
type trust-policy.json
You should see the JSON trust policy printed in your terminal, with lambda.amazonaws.com listed as the trusted service.
- Create the IAM role using the trust policy by running this command:
aws iam create-role --role-name task-tracker-lambda-role --assume-role-policy-document file://trust-policy.json
You will see a JSON response containing your new role's details, including an Arn field. Copy this ARN value and save it somewhere. You will need it shortly when creating the Lambda function.
What is a role ARN?
An ARN (Amazon Resource Name) is a unique address for any resource in AWS. Your role's ARN looks something like arn:aws:iam::123456789012:role/task-tracker-lambda-role. You will paste this ARN into the Lambda creation command so AWS knows which role your function should use.
- Attach the DynamoDB access policy to the role by running:
aws iam attach-role-policy --role-name task-tracker-lambda-role --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
- Attach the CloudWatch logging policy by running:
aws iam attach-role-policy --role-name task-tracker-lambda-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- Verify both policies are attached by running:
aws iam list-attached-role-policies --role-name task-tracker-lambda-role
You should see two policies listed: AmazonDynamoDBFullAccess and AWSLambdaBasicExecutionRole.
Only seeing one policy?
Make sure you ran both attach-role-policy commands. Each one attaches a different policy. If one command failed, re-run it and check the policy ARN for typos.
AWS needs a few seconds to propagate the new role. Wait about 10 seconds before moving on to the next substep.
- Wait 10 seconds for the role to propagate across AWS before continuing.
Write the Lambda function code
Your IAM role is ready. Now it is time to write the code that will run inside Lambda. This function will handle three operations: listing all tasks, creating a new task, and deleting a task.
The function uses the AWS SDK for JavaScript, which comes pre-installed in the Lambda runtime. You do not need to install any packages.
- Create a new file called index.mjs in your project folder. Open it in Notepad by running:
notepad index.mjs
Notepad will ask if you want to create a new file. Click Yes.
- Paste the following code into index.mjs. This sets up the DynamoDB connection and handles the GET /tasks route:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
ScanCommand,
PutCommand,
DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
// Create a DynamoDB client and document client
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const tableName = "task-tracker";
export const handler = async (event) => {
console.log("Received event:", JSON.stringify(event));
// Extract the HTTP method and path from the event
const method = event.requestContext?.http?.method || event.httpMethod;
const path = event.requestContext?.http?.path || event.path;
try {
// GET /tasks - return all tasks from DynamoDB
if (method === "GET" && path === "/tasks") {
const result = await docClient.send(
new ScanCommand({ TableName: tableName })
);
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tasks: result.Items }),
};
}
What does this code do?
The imports pull in the AWS SDK v3 DynamoDB client, which comes built into the Lambda Node.js runtime. You do not need to install anything.
The handler function is the entry point that Lambda calls every time a request arrives. It reads the HTTP method and path from the event to figure out which operation to run. The GET handler scans the entire task-tracker table and returns all items.
- Directly below the GET handler's closing brace, paste the POST handler:
// POST /tasks - create a new task
if (method === "POST" && path === "/tasks") {
const body = JSON.parse(event.body || "{}");
const item = {
taskId: randomUUID(),
title: body.title || "Untitled Task",
status: body.status || "todo",
createdAt: new Date().toISOString(),
};
await docClient.send(
new PutCommand({ TableName: tableName, Item: item })
);
return {
statusCode: 201,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Task created", task: item }),
};
}
What does the POST handler do?
When someone sends a POST request, this block parses the request body to get a title and status. It generates a unique taskId using randomUUID() and adds a timestamp. Then it writes the new task to DynamoDB.
- Directly below the POST handler, paste the DELETE handler and the error handling to complete the file:
// DELETE /tasks/{taskId} - remove a task by ID
if (method === "DELETE" && path.startsWith("/tasks/")) {
const taskId = path.split("/tasks/")[1];
await docClient.send(
new DeleteCommand({
TableName: tableName,
Key: { taskId },
})
);
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Task deleted", taskId }),
};
}
// No matching route found
return {
statusCode: 404,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Route not found" }),
};
} catch (error) {
console.error("Error:", error);
return {
statusCode: 500,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Internal server error" }),
};
}
};
What does the DELETE handler do?
The DELETE handler extracts the taskId from the URL path and removes that item from DynamoDB. The catch block at the bottom handles any unexpected errors and returns a clean 500 response instead of crashing.
- Save and close index.mjs in Notepad (Ctrl+S, then close the window).
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.mjs and closed Notepad before continuing.
ⓧ I'd like to double check the full code
Your index.mjs file has three main sections. Scroll through and confirm each one is present in this order:
- Imports and setup (lines 1-13): The three import statements, DynamoDBClient setup, and tableName constant.
- Handler function with GET, POST, and DELETE blocks, each wrapped in an if statement checking method and path.
- A 404 fallback return and a catch block returning a 500 error at the bottom.
- Package the function into a zip file by running:
Compress-Archive -Path index.mjs -DestinationPath function.zip
- Confirm the zip file exists by running:
ls
You should see function.zip, index.mjs, trust-policy.json, and cheat-sheet.txt listed in your project folder.
Don't see function.zip?
Make sure you are running the command from inside your project folder where index.mjs is located. If you see an error about the file not being found, run ls first to check which files are in your current directory.
Help me troubleshoot why my zip file was not created.
Deploy the function to AWS and test it
Your code is written and packaged. Now you need to get it to AWS. You will upload the zip file to an S3 bucket, then tell Lambda to load its code from that bucket.
- Create an S3 bucket to store your Lambda code by running this command (replace the suffix with your name and some random numbers to make the bucket name globally unique):
aws s3 mb s3://task-tracker-code-[[BUCKET_SUFFIX="your-unique-suffix e.g. jane-8392"]]
You should see a message confirming the bucket was created.
- Upload your zip file to the S3 bucket by running:
aws s3 cp function.zip s3://task-tracker-code-[[BUCKET_SUFFIX="your-unique-suffix e.g. jane-8392"]]/function.zip
You should see an upload progress indicator followed by a confirmation message.
Why upload to S3 first?
Lambda can load code directly from S3. By uploading your zip to a bucket, you keep a copy of your deployment package in the cloud. This is the standard workflow for deploying Lambda functions in production.
- Create the Lambda function by running this command. Replace the role ARN with the one you copied earlier when you created the IAM role:
aws lambda create-function --function-name task-tracker-function --runtime nodejs22.x --handler index.handler --role [[ROLE_ARN="arn:aws:iam::123456789012:role/task-tracker-lambda-role"]] --code S3Bucket=task-tracker-code-[[BUCKET_SUFFIX="your-unique-suffix e.g. jane-8392"]],S3Key=function.zip
You should see a JSON response with details about your new function, including its FunctionName, Runtime, and FunctionArn.
What does this command do?
This command tells AWS to create a new Lambda function called task-tracker-function. The --runtime nodejs22.x sets the Node.js version. The --handler index.handler tells Lambda to call the handler function exported from index.mjs.
The --code parameter points to your zip file in S3. Lambda downloads and unpacks it automatically.
Seeing an InvalidParameterValueException?
This usually means the role ARN is incorrect or the role has not finished propagating. Double-check the ARN for typos. If you just created the role, wait another 10 seconds and try again.
If you see a bucket-related error, make sure the bucket name in the --code parameter matches the exact bucket name you created earlier.
Help me fix my Lambda creation error.
Now for the moment of truth. Before you run the test, think about this: what do you expect to see in the response? Remember, you added a test task to DynamoDB in Step 2.
- Test your Lambda function by running this command:
aws lambda invoke --function-name task-tracker-function --cli-binary-format raw-in-base64-out --payload '{"httpMethod":"GET","path":"/tasks"}' response.json
- View the response by running:
type response.json
You should see a JSON response containing your test task from Step 2, with taskId of test-001, title of Learn AWS, and status of in-progress.
Seeing an error in response.json?
If the response contains an errorMessage, check that your IAM role has both policies attached (run aws iam list-attached-role-policies --role-name task-tracker-lambda-role to verify).
If the response says the function timed out, try running the invoke command again. The first invocation of a new Lambda function (called a "cold start") can sometimes take longer.
Help me debug my Lambda function response.
- Update your cheat-sheet.txt with your Lambda entry. Open the file by running:
notepad cheat-sheet.txt
- Add this line at the bottom of the file:
Lambda: A vending machine for code. You put in a request, your code runs for exactly as long as it needs, and you pay only for those seconds. No server to manage.
- Save and close cheat-sheet.txt.
Your Lambda function is deployed and proven to work. It can read tasks from DynamoDB, and you tested it with a live invocation. Next up, you will give this function a public URL so anyone can hit it from a browser.
Connect Your API and Hit It From Anywhere
In the last step, you deployed your Lambda function and proved it works by invoking it directly from the CLI. That is great for testing, but real users and apps do not have access to your CLI.
They need a URL they can call from a browser, a mobile app, or any HTTP client. API Gateway creates that public URL and routes incoming requests to your Lambda function. This is the moment your project goes from a function you can test to a live API anyone can hit.
In this step, get ready to:
- Create an HTTP API in API Gateway and get a public invoke URL.
- Wire up routes for GET, POST, and DELETE to your Lambda function.
- Test your live API from a browser and PowerShell.
Create the HTTP API
An HTTP API is a lightweight, low-cost entry point that accepts HTTP requests and forwards them to your backend. You will create one with a single CLI command.
- Create the API by running this command in PowerShell:
aws apigatewayv2 create-api --name task-tracker-api --protocol-type HTTP
What does this command do?
It creates a new HTTP API named task-tracker-api in your AWS account. The --protocol-type HTTP flag tells AWS to create the newer, cheaper HTTP API type instead of the older REST API.
You should see JSON output that includes an ApiId and an ApiEndpoint URL.
- Copy the ApiId value (a short string like a1b2c3d4e5) and save it somewhere. You will use it in every command for the rest of this step.
- Also copy the ApiEndpoint URL. This is your public API URL.
Seeing an error?
Make sure you are running the command in the same PowerShell window where you configured the AWS CLI. If you opened a new window, your credentials may not be loaded.
help me troubleshoot why my aws apigatewayv2 create-api command is failing
Create the Lambda integration and connect routes
The API exists, but it does not know where to send requests yet. You need to create an integration that points to your Lambda function, then create routes that map HTTP methods to that integration.
- Create the Lambda integration by running this command:
aws apigatewayv2 create-integration `
--api-id [[API_ID="your API ID from the previous command"]] `
--integration-type AWS_PROXY `
--integration-uri arn:aws:lambda:us-east-1:[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:function:task-tracker-function `
--payload-format-version 2.0
What does this command do?
It creates a link between your API and your Lambda function. AWS_PROXY means API Gateway passes the full HTTP request to Lambda and returns Lambda's response directly.
The --payload-format-version 2.0 uses the newer, simpler event format that your index.mjs function is already built to handle.
You should see JSON output containing an IntegrationId value.
- Copy the IntegrationId value. You will need it for the next three commands.
Find your account ID
If you do not remember your 12-digit AWS account ID, run aws sts get-caller-identity and look at the Account field in the output.
- Create the GET route by running this command:
aws apigatewayv2 create-route `
--api-id [[API_ID="your API ID"]] `
--route-key "GET /tasks" `
--target integrations/[[INTEGRATION_ID="your integration ID"]]
- Create the POST route by running this command:
aws apigatewayv2 create-route `
--api-id [[API_ID="your API ID"]] `
--route-key "POST /tasks" `
--target integrations/[[INTEGRATION_ID="your integration ID"]]
- Create the DELETE route by running this command:
aws apigatewayv2 create-route `
--api-id [[API_ID="your API ID"]] `
--route-key "DELETE /tasks/{taskId}" `
--target integrations/[[INTEGRATION_ID="your integration ID"]]
What do these routes do?
Each route maps an HTTP method and path to your Lambda integration. When someone sends a GET request to /tasks, API Gateway forwards it to your Lambda function. The same applies for POST /tasks (creating a task) and DELETE /tasks/{taskId} (removing one).
- Create a default stage with auto-deploy enabled by running:
aws apigatewayv2 create-stage `
--api-id [[API_ID="your API ID"]] `
--stage-name '$default' `
--auto-deploy
What is a stage?
A stage is like an environment label for your API (for example, dev, staging, production). The $default stage serves requests at the root of your URL, so you do not need to add a stage name in the path.
The --auto-deploy flag means any changes you make to routes or integrations go live automatically. No extra deployment step needed.
Your API is now wired up. Time to see if it works.
- Before you try this, think about what you expect to see. Do you think the request will succeed and return your tasks, or will something go wrong?
- Open your browser and paste your API URL followed by /tasks:
https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/tasks
You see {"message":"Internal Server Error"} instead of your tasks. That is completely expected. This is the designed obstacle for this step.
Why did this fail?
When you invoked the Lambda function directly in Step 3, it worked because YOU have admin permissions on your AWS account. But API Gateway is a different AWS service trying to call your function on your behalf.
API Gateway does not have permission to invoke your Lambda function yet. This is IAM in action. Every AWS service needs explicit permission to interact with other services, even within the same account.
Fix the permission gap and test your live API
The fix is a single command that adds a resource-based policy to your Lambda function, granting API Gateway permission to invoke it.
- Grant API Gateway permission to invoke your function by running:
aws lambda add-permission `
--function-name task-tracker-function `
--statement-id apigateway-invoke `
--action lambda:InvokeFunction `
--principal apigateway.amazonaws.com `
--source-arn "arn:aws:execute-api:us-east-1:[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:[[API_ID="your API ID"]]/*/*/*"
What does this command do?
It tells Lambda: allow the API Gateway service to call this function. The --source-arn restricts this permission to requests coming from your specific API.
The /*/*/* at the end means any stage, any method, and any route on that API.
You should see a JSON response containing the new policy statement.
- Go back to your browser and refresh the same URL you tried before.
This time you see a JSON response with your tasks from DynamoDB. Your API is live.
Still seeing Internal Server Error?
Double-check that the account ID and API ID in your --source-arn exactly match your values. A typo means the permission does not apply to your API.
Also confirm the --function-name is exactly task-tracker-function. If you get a message saying the statement ID already exists, that means the permission was already added successfully.
help me fix my API Gateway Internal Server Error when calling my Lambda function
- Test creating a new task by running this POST request in PowerShell:
Invoke-RestMethod `
-Uri "https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/tasks" `
-Method POST `
-ContentType "application/json" `
-Body '{"title": "Build a serverless API", "status": "done"}'
You should see a response with the message "Task created" and the new task object, including a generated taskId.
- Copy the taskId from the response.
- Test deleting that task by running this DELETE request:
Invoke-RestMethod `
-Uri "https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/tasks/[[TASK_ID="the taskId you just copied"]]" `
-Method DELETE
You should see a response with the message "Task deleted" and the taskId you specified.
- Confirm the delete worked by refreshing the /tasks URL in your browser. The task you just deleted should be gone from the list.
POST or DELETE returning an error?
Make sure your -ContentType is set to application/json for POST requests. For DELETE, make sure you replaced the taskId placeholder with the actual ID from your POST response.
help me debug why my POST or DELETE request to my API Gateway HTTP API is failing
- Open your cheat-sheet.txt file and add your API Gateway entry:
API Gateway: The front door of a restaurant. Customers come to the door, the door routes them to the right kitchen (Lambda), and brings back their order. Without it, nobody can find the kitchen.
- Save cheat-sheet.txt.
Your API is live, public, and fully functional. You can create, read, and delete tasks from a browser or any HTTP client. Next up, you will look under the hood to see how CloudWatch automatically captures every request your API handles.
Monitor Your API and Complete Your Services Cheat Sheet
In Step 4, you gave your Lambda function a public URL through API Gateway. You can now hit a live endpoint from a browser or terminal and see tasks come back from DynamoDB.
Your API works, but you have no idea what is happening inside it. In production, knowing that something works is not enough. You need to see how fast it runs, whether errors are piling up, and what each request looked like. CloudWatch captures all of this automatically because your function's IAM role includes the right logging policy.
In this step, get ready to:
- View CloudWatch logs for your Lambda function invocations.
- Check built-in Lambda metrics in the CloudWatch console.
- Complete your services cheat sheet with all 10 core AWS services.
View your Lambda logs in CloudWatch
Every time your Lambda function runs, it writes a log entry to CloudWatch Logs. These entries include the event data your code received, the HTTP method, and the response it returned. This happens automatically because your task-tracker-lambda-role role has the AWSLambdaBasicExecutionRole policy attached.
- Open the AWS Management Console by navigating to console.aws.amazon.com in your browser.
- In the search bar at the top, type CloudWatch and select it from the results.
- In the left sidebar, expand the Logs section and click Log groups.
- Find the log group named /aws/lambda/task-tracker-function and click on it.
Before you click into a log stream, what do you expect the logs to contain? Think about the console.log statement in your index.mjs file and the requests you sent in Step 4.
- Click on the most recent log stream at the top of the list.
You should see log entries showing the full event payload for each API request you made. Each entry includes the HTTP method, the request path, and the JSON response your function returned.
What are CloudWatch log streams?
A log group collects all logs from a single source, like your Lambda function. Inside the group, each execution environment creates a separate log stream.
The console.log("Received event:", JSON.stringify(event)) line in your index.mjs is what produces the detailed event data you see here. Every console.log and console.error call in your Lambda code gets captured automatically.
Don't see any log streams?
Make sure you sent at least one request to your API in Step 4. If the log group is empty, open your API's invoke URL in a browser tab to trigger a GET request, then refresh the CloudWatch page.
If you do not see the log group at all, confirm you are in the us-east-1 region. Check the region selector in the top right corner of the console.
Why aren't my Lambda CloudWatch logs showing up?
- Add your CloudWatch entry to cheat-sheet.txt:
CloudWatch: The flight recorder on an airplane. It automatically records everything that happens so you can go back and investigate if something goes wrong.
What does this analogy mean?
A flight recorder captures every detail during a flight so investigators can piece together what happened after the fact. CloudWatch does the same for your AWS resources. It records logs, tracks metrics, and can trigger alarms when something goes wrong.
- Save cheat-sheet.txt.
Check your Lambda metrics
Beyond logs, CloudWatch also tracks performance metrics for your Lambda function. These metrics tell you how many times your function ran, how long each run took, and whether any errors occurred.
- In the CloudWatch console left sidebar, click All metrics under the Metrics section.
- In the Browse tab, find and click Lambda.
- Click By Function Name.
- Find task-tracker-function and select the checkboxes for Invocations, Duration, and Errors.
You should see data points on the graph for each time you invoked your function. The duration metric shows how many milliseconds each invocation took to complete.
Why did these metrics appear automatically?
You did not configure any of this. AWS publishes basic monitoring metrics for every Lambda function at no extra cost. CloudWatch basic monitoring is part of the Always Free tier.
In a production environment, you would set alarms on these metrics. For example, you could trigger an alert if errors spike above zero or if average duration exceeds a threshold you define.
Complete your services cheat sheet
You have now used 6 AWS services hands-on in this project. Five more core services round out the top 10 that appear most often in AWS job listings and certifications.
These 5 services are the traditional-architecture counterparts to the serverless stack you just built. You would use them when you need more control, run long-lived workloads, or manage relational databases.
- Add the following 5 entries to your cheat-sheet.txt:
EC2: Your own computer in the cloud that you fully control. Use when you need to install custom software or run 24/7 workloads.
VPC: A private, fenced-off section of the AWS cloud. Like having your own floor in a shared office building.
RDS: A managed database with a traditional SQL engine. Like hiring a DBA who handles backups and updates while you just write queries.
ELB: A traffic cop that spreads incoming visitors across multiple servers so no single one gets overwhelmed.
Route 53: The phone book of the internet. It translates your domain name into the IP address where your app lives.
How do these services compare to what you built?
- EC2 is the server-based alternative to Lambda. Instead of running code only when a request arrives, EC2 gives you a full virtual machine running 24/7.
- VPC is the private network layer that EC2 instances sit inside. It controls who can reach your servers and how they communicate with each other.
- RDS is a managed relational database (PostgreSQL, MySQL). Your project used DynamoDB (NoSQL) instead, which trades SQL flexibility for automatic scaling and zero maintenance.
- ELB distributes traffic across multiple EC2 instances so no single server gets overwhelmed. API Gateway plays a similar routing role for serverless architectures.
- Route 53 maps domain names to your resources. You would use it to give your API a custom domain like api.yourdomain.com instead of the auto-generated API Gateway URL.
- Save cheat-sheet.txt.
- Open the file and confirm it now has entries for all the services you covered: AWS CLI, IAM, DynamoDB, S3, Lambda, API Gateway, CloudWatch, EC2, VPC, RDS, ELB, and Route 53.
Keep this cheat sheet handy
This cheat sheet is a quick reference you can pull out before an interview or certification exam. Each analogy gives you a plain-English way to explain what a service does to a non-technical person.
Secret mission
Perform a FinOps Cost Analysis of Your API
You built a working serverless API. Now prove you can think about what it costs. Open the AWS Billing console, calculate what your API would cost at 1,000,000 requests per month, and write a FinOps recommendation that shows recruiters you care about cloud spending, not just cloud building.
Clean Up Your Resources
Clean Up Your Resources
This project uses serverless AWS services that bill only when called. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Your AWS free tier has expired, so any usage is billed at on-demand rates. Lambda, API Gateway, and DynamoDB all bill per-use, meaning idle resources cost essentially nothing.
The only ongoing charge is minimal S3 storage for your Lambda code zip file (fractions of a penny per month). If you do not plan to continue building on this project, deleting everything is recommended.
Resources you used:
- DynamoDB table: task-tracker (on-demand billing, costs only when read or written).
- Lambda function: task-tracker-function (costs only when invoked).
- API Gateway HTTP API: task-tracker-api (costs only when called).
- IAM role: task-tracker-lambda-role (free).
- S3 bucket: task-tracker-code-{suffix} (minimal storage cost for the zip file).
- CloudWatch log group: /aws/lambda/task-tracker-function (covered by the Always Free tier for basic logging).
- Local project folder on your Desktop containing trust-policy.json, index.mjs, and cheat-sheet.txt.
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend this API soon.
- All services in this project bill per-use. An idle API with no incoming requests costs essentially nothing.
- The only ongoing charge is minimal S3 storage for your Lambda code zip file (fractions of a penny per month).
- Your API endpoint URL remains live. You can continue sending requests at any time.
- Your AWS CLI installation stays on your machine for future projects.
Pause - I'll come back to this later
Serverless services have no running processes to stop. They are already idle when not in use, so there is nothing to shut down.
- Save your API endpoint URL somewhere handy so you can pick up where you left off.
- Lambda, API Gateway, and DynamoDB only incur charges when they receive requests. Leaving them idle costs nothing.
- Your local project files on the Desktop can stay as they are.
- When you return, you can resume sending requests to your API right away. No restart needed.
Delete - I don't want to use this again
Remove all project resources and start fresh. Run the following commands in PowerShell to delete everything.
- Delete the API Gateway HTTP API, Lambda function, DynamoDB table, and S3 bucket by running these commands:
aws apigatewayv2 delete-api --api-id [[API_ID="your API ID from Step 4"]]
aws lambda delete-function --function-name task-tracker-function
aws dynamodb delete-table --table-name task-tracker
aws s3 rb s3://task-tracker-code-[[S3_SUFFIX="your unique suffix"]] --force
- Detach the IAM role policies, delete the role, and delete the CloudWatch log group by running these commands:
aws iam detach-role-policy --role-name task-tracker-lambda-role --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam detach-role-policy --role-name task-tracker-lambda-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name task-tracker-lambda-role
aws logs delete-log-group --log-group-name /aws/lambda/task-tracker-function
Save your cheat sheet first
Before deleting your local project folder, consider copying cheat-sheet.txt and your FinOps cost analysis to a safe location. These are useful references for interviews and future projects.
- Delete your local project folder from your Desktop.
macOS
rm -rf ~/Desktop/[[PROJECT_FOLDER="your project folder name"]]
Windows
Remove-Item -Recurse -Force ~\Desktop\[[PROJECT_FOLDER="your project folder name"]]
Nice Work!
Nice Work!
Nice work! You just built a live serverless REST API on AWS, tested it from a browser and terminal, and explained every service behind it in plain English.
You've learned how to:
- Build a serverless API using Lambda, API Gateway, and DynamoDB that handles creating, reading, and deleting tasks through a public URL.
- Secure your function with an IAM execution role, deploy your code through S3, and monitor every request with CloudWatch logs and metrics.
- Write plain-English analogies for all 10 core AWS services (Lambda, API Gateway, DynamoDB, IAM, CloudWatch, S3, EC2, VPC, RDS, ELB, Route 53) that you can pull out in any interview.
- Secret Mission: Perform a FinOps cost analysis calculating your actual project costs, projecting costs at 1 million requests per month, and writing optimization recommendations.
Ready to quiz yourself?