Build a Serverless Task API

Wire Lambda, API Gateway, and DynamoDB into a live REST API.

Introduction

30 Second Summary

Every app you use daily has an API behind it that manages your data. When you add a task, update your profile, or delete a post, an API handles that request somewhere in the cloud.

In this project, you will build a serverless REST API on AWS that creates, reads, updates, and deletes tasks. You'll wire together three core AWS services to create a live endpoint anyone can reach from the internet.

What You'll Build

A fully functional serverless Task API that accepts HTTP requests at a public URL and stores task data in a cloud database, with no servers to manage.

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

  • A DynamoDB table storing task data in the cloud.
  • A Lambda function handling all create, read, update, and delete logic in JavaScript.
  • A live API Gateway endpoint you can hit from your terminal with curl.
  • Secret Mission: Add task filtering so you can query tasks by status.

Are there any prerequisites?

You'll need an AWS account to complete this project. All resources stay within the AWS Free Tier, so you won't be charged.

You'll also need a macOS or Windows computer with a terminal that has curl installed (macOS includes it by default).

Create Your DynamoDB Table

Every API needs somewhere to store its data. Your Task API will use DynamoDB, AWS's fully managed NoSQL database, to persist tasks.

In this step, you'll create the DynamoDB table that holds your task data. This is the foundation your Lambda function will read from and write to in later steps.

In this step, get ready to:
  • Navigate to the DynamoDB console in AWS.
  • Create a table named Tasks with the right partition key and capacity settings.
Navigate to the DynamoDB console

The AWS Management Console is your central hub for managing all AWS services. You need to find DynamoDB within it.

  • Open your browser and go to https://console.aws.amazon.com.
  • Sign in with your AWS account credentials.
  • In the search bar at the top of the console, type DynamoDB.
  • Click DynamoDB from the search results to open the DynamoDB console.

What is DynamoDB?

DynamoDB is a fully managed NoSQL database service from AWS. Unlike traditional SQL databases where you define rigid column schemas, DynamoDB stores items (rows) that can have flexible attributes (columns).

You only need to define the primary key upfront. Everything else can vary from item to item.

Create the Tasks table

Your table needs a partition key that uniquely identifies each task. You'll use a string field called id so each task gets a unique identifier.

  • In the left navigation pane, click Tables.
  • Click Create table.
  • For Table name, enter Tasks.
  • For Partition key, enter id.
  • Leave the partition key type set to String.
  • Leave the Sort key field empty.

Why a String partition key?

Your Lambda function will generate a UUID (universally unique identifier) for each task. UUIDs are strings like a1b2c3d4-e5f6-..., so the partition key type must be String to match.

Set capacity mode to Provisioned

DynamoDB offers two capacity modes: On-Demand (pay per request) and Provisioned (fixed read/write throughput). You'll use Provisioned because the DynamoDB Always Free tier covers provisioned capacity specifically.

  • In the Table settings section, select Customize settings.
  • Under Read/write capacity settings, confirm Capacity mode is set to Provisioned.
  • Set Read capacity to 5 units.
  • Turn off Auto scaling for Read capacity (uncheck it if enabled).
  • Set Write capacity to 5 units.
  • Turn off Auto scaling for Write capacity (uncheck it if enabled).
  • Scroll to the bottom and click Create table.

Why Provisioned instead of On-Demand?

The DynamoDB Always Free tier gives you 25 Read Capacity Units and 25 Write Capacity Units per month on provisioned tables. On-Demand mode does not have a permanent Always Free allowance.

By choosing Provisioned with 5 RCU and 5 WCU, you stay well within the free tier. This means zero cost for this project.

  • Wait for the table status to change from Creating to Active.

This usually takes about 10-20 seconds. You can see the status in the Tables list or on the table's detail page.

Table stuck on Creating?

If the table doesn't become Active within a minute, try refreshing the page. The status indicator sometimes needs a manual refresh to update.

If you see an error about permissions, make sure you're signed in with an account that has DynamoDB access (the root account or an IAM user with DynamoDB permissions).

help me troubleshoot why my DynamoDB table is not being created

Your database is ready. Next up, you'll create the Lambda function that reads from and writes to this table.

Create Your Lambda Function

Your DynamoDB table is ready to store task data. Now you need something that can receive requests, decide what to do with them, and talk to that table.

AWS Lambda lets you run code without managing servers. You write a function, upload it, and Lambda handles everything else. In this step, you'll create a Lambda function and give it permission to access your DynamoDB table.

In this step, get ready to:
  • Create a Lambda function with the Node.js 22.x runtime.
  • Attach DynamoDB permissions to the function's IAM role.
Create the TaskApiHandler function

Every Lambda function needs a runtime (the language it runs) and an execution role (the permissions it has). You'll create a function called TaskApiHandler that uses Node.js and automatically gets a basic execution role.

  • In the AWS Console, use the search bar at the top to search for Lambda.
  • Select Lambda from the results to open the Lambda console.
  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter TaskApiHandler.
  • For Runtime, choose Node.js 22.x.
  • Leave the architecture set to x86_64.
  • Under Permissions, expand Change default execution role.
  • Confirm that Create a new role with basic Lambda permissions is selected.
  • Click Create function.

Lambda creates your function and opens its detail page. You should see a green banner confirming the function was created successfully.

What just happened?

Lambda created two things for you: the function itself (with a placeholder index.mjs file) and an IAM role. The role's name starts with TaskApiHandler-role- followed by a random suffix.

This role currently only has permission to write logs to CloudWatch. It cannot read or write to your DynamoDB table yet.

Don't see Node.js 22.x in the Runtime dropdown?

Make sure you are in a region that supports the nodejs22.x runtime. Try switching to us-east-1 (N. Virginia) using the region selector in the top-right corner of the console.

Help me find the Node.js 22.x runtime in Lambda

Attach DynamoDB permissions to the IAM role

Your Lambda function needs permission to read and write items in your Tasks table. AWS services don't automatically trust each other. You grant access by attaching a permissions policy to the function's IAM role.

  • In the AWS Console search bar, search for IAM.
  • Select IAM to open the IAM console.
  • In the left navigation pane, click Roles.
  • In the search box, type TaskApiHandler to filter the roles list.
  • Click the role name that starts with TaskApiHandler-role-.
  • Select the Permissions tab.
  • Click Add permissions.
  • Choose Attach policies.
  • In the search box, type AmazonDynamoDBFullAccess.
  • Select the checkbox next to AmazonDynamoDBFullAccess.
  • Click Add permissions.

You should see a success banner confirming the policy was attached. The Permissions tab now shows two policies: the basic Lambda execution policy and AmazonDynamoDBFullAccess.

Why AmazonDynamoDBFullAccess?

This is an AWS managed policy that grants full read/write access to all DynamoDB tables in your account. For a learning project, it keeps things simple.

In production, you would scope this down to only the Tasks table using a custom policy (the principle of least privilege).

Can't find the role?

The role name is auto-generated by Lambda and includes a random suffix. Make sure you're searching for TaskApiHandler (not the full role name). If nothing appears, confirm you created the Lambda function in the same region you're viewing in IAM.

Help me find my Lambda role in IAM

Your Lambda function now has the permissions it needs to read and write to DynamoDB. Next, you'll write the actual handler code that processes API requests and performs CRUD operations on your Tasks table.

Write Your API Handler Code

Your Lambda function exists, and it has permission to talk to your DynamoDB table. Right now the function only has placeholder code that does nothing useful.

In this step, you will write the actual handler code that receives HTTP requests and performs create, read, update, and delete operations against your Tasks table.

In this step, get ready to:
  • Write a CRUD handler that routes requests by HTTP method and path.
  • Deploy and test the function to confirm it runs without errors.
Replace the default code with your CRUD handler

The AWS SDK v3 is already included in all Node.js Lambda runtimes. Your code will use the DynamoDB Document Client, which lets you work with normal JavaScript objects instead of raw DynamoDB attribute maps.

  • Return to your TaskApiHandler function in the Lambda console.
  • Click the Code tab to open the inline code editor.
  • Select all the existing code in index.mjs and delete it.
  • Paste in the following imports and client setup:
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
  ScanCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";

// Create the DynamoDB Document Client
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = "Tasks";

What does this code do?

  • DynamoDBClient is the low-level SDK client. DynamoDBDocumentClient wraps it so you can read and write plain JavaScript objects.
  • PutCommand, GetCommand, ScanCommand, and DeleteCommand are the DynamoDB operations your API will use.
  • randomUUID generates unique IDs for new tasks.
  • TABLE_NAME is set to "Tasks" so every function references the same table.
  • Directly below what you just pasted, add the handler function and response helper:
export const handler = async (event) => {
  // Destructure the fields API Gateway sends
  const { routeKey, pathParameters, body } = event;

  try {
    switch (routeKey) {
      case "GET /tasks":
        return await listTasks();
      case "GET /tasks/{id}":
        return await getTask(pathParameters.id);
      case "POST /tasks":
        return await createTask(JSON.parse(body));
      case "PUT /tasks/{id}":
        return await updateTask(pathParameters.id, JSON.parse(body));
      case "DELETE /tasks/{id}":
        return await deleteTask(pathParameters.id);
      default:
        return response(400, { message: "Unsupported route" });
    }
  } catch (error) {
    return response(500, { message: error.message });
  }
};

// Helper to build a consistent HTTP response object
function response(statusCode, body) {
  return {
    statusCode,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  };
}

What does the handler do?

API Gateway sends an event object with a routeKey field like "GET /tasks" or "POST /tasks". The switch statement reads this field and calls the matching CRUD function.

The response helper ensures every return value has a statusCode, headers, and JSON-stringified body. This is the format API Gateway expects.

  • Below the response function, paste the two read operations:
// Return all tasks from the table
async function listTasks() {
  const result = await docClient.send(new ScanCommand({ TableName: TABLE_NAME }));
  return response(200, result.Items);
}

// Return a single task by its ID
async function getTask(id) {
  const result = await docClient.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  if (!result.Item) {
    return response(404, { message: "Task not found" });
  }
  return response(200, result.Item);
}

How do the read operations work?

  • listTasks uses ScanCommand to read every item in the table and returns them as an array.
  • getTask uses GetCommand with the specific id to fetch one item. If the item does not exist, it returns a 404 response.
  • Finally, paste the three write operations below getTask:
// Create a new task with a generated ID and timestamp
async function createTask(data) {
  const task = {
    id: randomUUID(),
    title: data.title,
    status: data.status || "pending",
    createdAt: new Date().toISOString(),
  };
  await docClient.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: task,
    })
  );
  return response(201, task);
}

// Update an existing task by merging new data
async function updateTask(id, data) {
  const existing = await docClient.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  if (!existing.Item) {
    return response(404, { message: "Task not found" });
  }
  const updated = { ...existing.Item, ...data, id };
  await docClient.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: updated,
    })
  );
  return response(200, updated);
}

// Delete a task by its ID
async function deleteTask(id) {
  await docClient.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  return response(200, { message: "Task deleted" });
}

How do the write operations work?

  • createTask builds a new item with a random UUID, the submitted title, a default status of "pending", and a timestamp. It writes the item with PutCommand.
  • updateTask first checks that the task exists. If it does, it merges the new data into the existing item and writes it back. The id is always preserved so a caller cannot overwrite it.
  • deleteTask removes the item from the table and returns a confirmation message.

✔️ Awesome, I've got everything!

Great. Double check you have all the code in the right order before deploying.

ⓧ I'd like to double check the full code

Here is the complete index.mjs file. Make sure yours matches exactly:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  GetCommand,
  ScanCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = "Tasks";

export const handler = async (event) => {
  const { routeKey, pathParameters, body } = event;

  try {
    switch (routeKey) {
      case "GET /tasks":
        return await listTasks();
      case "GET /tasks/{id}":
        return await getTask(pathParameters.id);
      case "POST /tasks":
        return await createTask(JSON.parse(body));
      case "PUT /tasks/{id}":
        return await updateTask(pathParameters.id, JSON.parse(body));
      case "DELETE /tasks/{id}":
        return await deleteTask(pathParameters.id);
      default:
        return response(400, { message: "Unsupported route" });
    }
  } catch (error) {
    return response(500, { message: error.message });
  }
};

function response(statusCode, body) {
  return {
    statusCode,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  };
}

async function listTasks() {
  const result = await docClient.send(new ScanCommand({ TableName: TABLE_NAME }));
  return response(200, result.Items);
}

async function getTask(id) {
  const result = await docClient.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  if (!result.Item) {
    return response(404, { message: "Task not found" });
  }
  return response(200, result.Item);
}

async function createTask(data) {
  const task = {
    id: randomUUID(),
    title: data.title,
    status: data.status || "pending",
    createdAt: new Date().toISOString(),
  };
  await docClient.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: task,
    })
  );
  return response(201, task);
}

async function updateTask(id, data) {
  const existing = await docClient.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  if (!existing.Item) {
    return response(404, { message: "Task not found" });
  }
  const updated = { ...existing.Item, ...data, id };
  await docClient.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: updated,
    })
  );
  return response(200, updated);
}

async function deleteTask(id) {
  await docClient.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: { id },
    })
  );
  return response(200, { message: "Task deleted" });
}
Deploy and test your function

Code changes in the Lambda editor are not live until you deploy them. After deploying, you will create a test event that simulates an API Gateway HTTP request.

  • Click the Deploy button above the code editor to save your function.
  • Click the Test tab.
  • Under Test event, select Create new event.
  • Enter ListTasksEvent as the event name.
  • Replace the default JSON with the following test payload:
{
  "routeKey": "GET /tasks",
  "pathParameters": null,
  "queryStringParameters": null,
  "body": null
}

What is this test event?

This JSON mimics what API Gateway sends to your function when someone makes a GET /tasks request. The routeKey tells your handler which CRUD operation to run.

Since your Tasks table is empty, you should get back an empty list. That is the correct result.

  • Click Save to save the test event.
  • Click Test to invoke the function.
  • Under Execution result, expand Details. You should see a response with statusCode 200 and an empty array [] in the body.

Seeing an error instead of a 200 response?

  • If you see a syntax error, double check that your code matches the full code tab above. A missing comma or bracket is the most common cause.
  • If you see an access denied error, confirm that the AmazonDynamoDBFullAccess policy is attached to your function's IAM role (from Step 2).
  • Make sure you clicked Deploy before clicking Test. Testing without deploying runs the old placeholder code.

Help me debug my Lambda test result.

Your Lambda function is deployed and verified. Next up, you will connect it to API Gateway so it can receive real HTTP requests from the internet.

Connect API Gateway

Your Lambda function can handle any CRUD request you throw at it. The problem is that it currently lives in isolation. There's no way for the outside world to reach it.

That's where API Gateway comes in. It creates a public URL that anyone on the internet can call, and routes those HTTP requests to your Lambda function.

In this step, get ready to:
  • Create an HTTP API with a Lambda integration.
  • Configure five routes for your CRUD operations.
  • Note your live API invoke URL.
Create an HTTP API with a Lambda integration

An HTTP API is a lightweight API type in API Gateway. It gives your Lambda function a public endpoint that accepts HTTP requests and automatically deploys changes.

  • In the AWS Console search bar, type API Gateway and select the service.
  • Click Create API.
  • Find the HTTP API card and click Build.
  • On the Create and configure integrations page, click Add integration.
  • Select Lambda from the integration type dropdown.
  • In the Lambda function field, search for and select TaskApiHandler.
  • For API name, enter TaskApi.
  • Click Next.
  • On the Configure routes page, click Next to skip route creation for now.
  • On the Define stages page, keep the default $default stage and click Next.
  • Review your settings, then click Create.

What just happened?

You created an HTTP API with a Lambda integration. This means API Gateway now has permission to invoke your TaskApiHandler function. It also created a default stage that auto-deploys any changes you make.

The integration uses payload format version 2.0, which is why your Lambda function receives event.routeKey, event.pathParameters, and event.body directly.

Why HTTP API instead of REST API?

API Gateway offers two types of RESTful APIs. HTTP APIs are newer, simpler to configure, and cheaper ($1 per million requests vs $3.50 for REST APIs). They also provide the clean payload format 2.0 that makes routing logic straightforward in your Lambda code.

Configure routes for your CRUD operations

Routes tell API Gateway which requests to forward to your Lambda function. Each route is a combination of an HTTP method and a resource path. You need five routes to cover all your CRUD operations.

  • In the left sidebar of your API, click Routes.
  • Click Create.
  • For Method, select GET.
  • For the path, enter /tasks.
  • Click Create.

Repeat the same steps to create the remaining four routes:

  • Method: GET, Path: /tasks/{id}.
  • Method: POST, Path: /tasks.
  • Method: PUT, Path: /tasks/{id}.
  • Method: DELETE, Path: /tasks/{id}.

Now attach your Lambda integration to each route so they all invoke the same function.

  • In the left sidebar, click Integrations.
  • Click the GET /tasks route.
  • Under Choose an existing integration, select TaskApiHandler.
  • Click Attach integration.
  • Repeat for the remaining four routes: GET /tasks/{id}, POST /tasks, PUT /tasks/{id}, and DELETE /tasks/{id}.

What does {id} mean in the path?

The {id} portion is a path parameter. When a client sends a request to /tasks/abc123, API Gateway extracts abc123 and passes it to your Lambda function as event.pathParameters.id. This is how your getTask, updateTask, and deleteTask functions know which task to operate on.

You should now see all five routes listed on the Integrations page, each showing a Lambda integration attached.

Routes not showing an integration?

Make sure you clicked Attach integration for each route individually. If a route still shows no integration, click it, select TaskApiHandler from the dropdown, and click Attach integration again.

Why aren't my routes attaching to the Lambda integration?

Note your API invoke URL

Your API is now live. API Gateway automatically deployed it to the default stage, which means it already has a public URL you can call.

  • In the left sidebar, click API: TaskApi to return to the main details page.
  • Find the Invoke URL displayed under the $default stage on the details page.
  • Copy this URL and save it somewhere accessible (a text file, sticky note, or just keep the tab open).

What does the invoke URL look like?

Your invoke URL follows this pattern:

https://<api-id>.execute-api.<region>.amazonaws.com

The <api-id> is a unique identifier AWS assigns to your API. The <region> matches the AWS region where you created it. You'll use this URL with your route paths (e.g., /tasks) in the next step.

Your serverless API is now fully wired. API Gateway routes HTTP requests to your Lambda function, which reads and writes data in DynamoDB. Next up, you'll test this entire flow with real HTTP requests from your terminal.

Test Your Live API

Your serverless architecture is fully wired. API Gateway routes requests to your Lambda function, which reads and writes data in DynamoDB.

Now it's time to prove it works. You'll send real HTTP requests from your Terminal to the live API endpoint and watch data flow through all three services.

In this step, get ready to:
  • Create a task using a POST request and verify it gets an auto-generated ID.
  • List all tasks and retrieve a single task by its ID.
  • Delete a task and confirm it's gone.
Create a task with a POST request

The curl command lets you send HTTP requests straight from your Terminal. You'll use it to hit your live API endpoint and create your first task.

  • Press Cmd+Space to open Spotlight Search.
  • Type Terminal and press Enter to open it.
  • Create your first task by running this command:
curl -X POST [[INVOKE_URL="your API invoke URL"]]/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Buy groceries", "status": "pending"}'

What does this command do?

This sends a POST request to your API's /tasks route with a JSON body containing a title and status.

Your Lambda function receives this request, generates a unique ID using randomUUID(), and stores the task in DynamoDB.

You should see a JSON response containing your new task with an auto-generated id, the title you sent, a status of pending, and a createdAt timestamp.

Copy the task ID

Copy the id value from the response. You'll need it for the next commands to retrieve and delete this specific task.

Seeing an error instead of a JSON response?

Make sure you replaced the invoke URL placeholder with your actual API URL from the previous step. It should look like https://abc123.execute-api.us-east-1.amazonaws.com.

If you see a 500 error, return to the IAM console and confirm the AmazonDynamoDBFullAccess policy is attached to your Lambda's role.

help me debug my curl POST request to the API

List all tasks and retrieve one by ID

Your API supports two types of GET requests. One returns all tasks in the table, and the other returns a single task by its ID.

  • List all tasks in your DynamoDB table by running:
curl [[INVOKE_URL="your API invoke URL"]]/tasks

You should see a JSON array containing the task you just created.

  • Retrieve a single task by its ID by running:
curl [[INVOKE_URL="your API invoke URL"]]/tasks/[[TASK_ID="your task ID from the POST response"]]

What's the difference between these two routes?

GET /tasks triggers a ScanCommand that returns every item in the table. GET /tasks/{id} triggers a GetCommand that fetches one specific item by its partition key.

In a real application, the single-item lookup is far more efficient because it targets one record directly instead of scanning the entire table.

The single-task response should show the same task object you saw in the POST response, with all four fields intact.

Getting a 'Task not found' response?

Double-check that the ID you pasted matches the exact id value from the POST response. UUIDs are long strings and easy to copy incorrectly.

help me figure out why my GET request returns Task not found

Delete a task and confirm removal

The final operation to test is DELETE. You'll remove the task you created and then verify it no longer appears in the list.

  • Delete your task by running:
curl -X DELETE [[INVOKE_URL="your API invoke URL"]]/tasks/[[TASK_ID="your task ID from the POST response"]]

You should see a JSON response with the message Task deleted.

  • Confirm the task is gone by listing all tasks again:
curl [[INVOKE_URL="your API invoke URL"]]/tasks

You should see an empty array [], confirming the task was removed from DynamoDB.

What just happened end to end?

Each curl command sent an HTTP request over the internet to your API Gateway URL. API Gateway matched the request method and path to a route, then forwarded the event to your Lambda function.

Your Lambda function read event.routeKey to decide which operation to run, then used the DynamoDB Document Client to read or write data. The response flowed back through API Gateway to your Terminal.

Task still showing after delete?

Make sure the ID in your DELETE command exactly matches the task's id. If you accidentally typed a different ID, the delete succeeded against a non-existent key without error.

help me figure out why my task wasn't deleted

Secret mission

Add Task Filtering by Status

Your API returns all tasks every time. Real APIs let users filter results. Add query string parameter support so GET /tasks?status=pending returns only tasks matching that status.

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. Lambda and DynamoDB are covered by the AWS Always Free tier, so they cost nothing during normal usage. API Gateway is free for 12 months from account creation.

Cost warning

Your API Gateway HTTP API is covered by the free tier for 12 months from your AWS account creation date (1 million HTTP API calls per month). After those 12 months, idle resources generate no cost because you're only charged per request. However, if you're past the 12-month mark and still receiving requests, you'll pay $1 per million calls.

DynamoDB and Lambda are Always Free (not time-limited), so they cost nothing as long as you stay within the free tier limits.

Resources you used:

  • DynamoDB table Tasks (Always Free tier: 25 RCU/WCU + 25 GB storage).
  • Lambda function TaskApiHandler (Always Free tier: 1 million requests/month).
  • API Gateway HTTP API (free for 12 months: 1 million HTTP API calls/month).
  • IAM role TaskApiHandler-role- (no cost).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your API.

  • All four resources stay within free tier for normal usage.
  • Lambda and DynamoDB are Always Free (no time limit).
  • API Gateway is free for 12 months from account creation. After that, you're only charged per request ($1 per million).
  • If you stop sending requests, the resources sit idle at zero cost.

Pause - I'll come back to this later

Since all resources are serverless, there are no running processes to stop. They only consume resources when a request comes in.

  • You can delete the test data from your DynamoDB table by navigating to the Tasks table in the DynamoDB console, selecting Explore table items, and deleting any items you created during testing.
  • Your API endpoint remains live, but with no clients calling it, there's zero cost.
  • When you return, everything will work exactly as you left it.

Delete - I don't want to use this again

Remove all project resources completely. Follow these steps in order.

Delete the HTTP API:

  • Open the API Gateway console at https://console.aws.amazon.com/apigateway.
  • On the APIs page, select your API.
  • Choose Actions, then choose Delete.
  • Choose Delete to confirm.

Delete the Lambda function:

  • Open the Lambda console at https://console.aws.amazon.com/lambda.
  • On the Functions page, select TaskApiHandler.
  • Choose Actions, then Delete.
  • Type confirm in the text input field and choose Delete.

Delete the DynamoDB table:

  • Open the DynamoDB console at https://console.aws.amazon.com/dynamodb.
  • In the left navigation pane, choose Tables.
  • Select the checkbox beside the Tasks table.
  • Choose Delete.
  • Confirm the deletion when prompted.

Delete the IAM role:

  • Open the IAM console at https://console.aws.amazon.com/iam.
  • In the left navigation pane, choose Roles.
  • Search for TaskApiHandler-role- and select the role.
  • Choose Delete.
  • Enter the name of the role in the text input field and choose Delete.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional serverless REST API on AWS. No servers to manage, no patches to apply, no scaling headaches to worry about.

You've learned how to:

  • Built a DynamoDB table and a Lambda function that handles full CRUD operations against it.
  • Connected API Gateway to expose your function as a public REST API accessible from anywhere on the internet.
  • Configured IAM roles to let AWS services communicate securely with each other.
  • Secret Mission: Added query filtering with DynamoDB FilterExpression so your API can return tasks by status.

Ready to quiz yourself?