Full-Stack App with React and FastAPI

Build and deploy a task manager app with React, FastAPI, and AWS.

Introduction

30 Second Summary

Every job listing in tech seems to want the same thing: someone who can build a frontend, connect it to a backend, and deploy the whole thing to the cloud. The trick is proving you can actually do it.

In this project, you will build a full-stack Task Manager application using React for the frontend and FastAPI for the backend, then deploy it live on AWS. By the end, you will have a public URL you can put on your resume.

What You'll Build

A full-stack Task Manager app where users can add tasks, mark them complete, and delete them, with a React frontend talking to a Python API running on AWS serverless infrastructure.

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

  • A React single-page app built with Vite that lets users create, complete, and delete tasks.
  • A Python FastAPI REST API with four CRUD endpoints and automatic interactive documentation.
  • A live deployment on AWS with the backend on AWS Lambda behind API Gateway, and the frontend on AWS Amplify with automatic CI/CD from GitHub.
  • Secret Mission: Add persistent storage with Amazon DynamoDB so your tasks survive between deployments.

Are there any prerequisites?

You should have basic Python knowledge and be comfortable using the command line. No prior React experience is needed.

Before We Start

Before We Start...

Set Up Your Project

Every full-stack app has two sides. A Python backend handles the data and logic. A React frontend handles what users see and interact with.

In this step, you'll create a clean project structure that keeps both sides separate. You'll also install the backend libraries and scaffold a fresh React app with Vite.

In this step, get ready to:
  • Create a task-manager project folder with backend and frontend subfolders.
  • Install FastAPI and Mangum in a Python virtual environment.
  • Scaffold a React app with Vite and verify both development servers start.
Create the project structure and Python backend environment

Your project needs two separate folders. The backend folder holds your Python API code. The frontend folder holds your React app.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder with backend and frontend subfolders by running these commands:
mkdir task-manager
cd task-manager
mkdir backend frontend
  • Confirm the folders exist by running:
ls

You should see backend and frontend in the output.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the task-manager folder on your Desktop, then click Open.

Python projects use virtual environments to keep their dependencies isolated from other projects on your computer. This prevents version conflicts between different projects.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, found above Tab).
  • Create a Python virtual environment and activate it by running these commands:

macOS

cd backend
python3 -m venv venv
source venv/bin/activate

Windows

cd backend
python -m venv venv
.\venv\Scripts\activate

What do these commands do?

  • cd backend moves into the backend folder.
  • python3 -m venv venv (or python -m venv venv on Windows) creates a virtual environment in a folder called venv.
  • The activate command turns on the virtual environment so that any packages you install go inside it.

You should see (venv) at the start of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

  • On Windows, make sure you ran .\venv\Scripts\activate with a backslash, not a forward slash.
  • On macOS, make sure you used source before the activate path.
  • If you see a permissions error on Windows, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and try again.

Help me activate my Python virtual environment.

Now you'll specify exactly which packages the backend needs. A requirements.txt file locks down the versions so every environment uses the same ones.

  • In VS Code's Explorer sidebar, right-click the backend folder.
  • Click New File.
  • Name the file requirements.txt and press Enter.
  • Paste the following into requirements.txt:
fastapi[standard]==0.136.1
mangum==0.21.0

What are these packages?

  • fastapi[standard] is the Python framework you'll use to build your REST API. The [standard] extra includes a development server with auto-reload.
  • mangum is an adapter that lets your FastAPI app run on AWS Lambda. You'll use it when you deploy in a later step.
  • Save requirements.txt.
  • Install the backend dependencies by running this command in your terminal:
pip install -r requirements.txt

You should see Successfully installed followed by package names at the end of the output.

Seeing an installation error?

  • Make sure (venv) is still showing in your terminal prompt. If it disappeared, re-run the activate command from above.
  • Check that you are inside the backend folder where requirements.txt lives.

Help me fix my pip install error.

Scaffold the React frontend with Vite

Vite is a build tool that creates a ready-to-go React project structure. It handles compiling, bundling, and hot reloading so you can see changes instantly during development.

  • Open a second terminal in VS Code by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
  • Navigate to the frontend folder by running:
cd frontend
  • Scaffold a new React project in this folder by running:

macOS

npm create vite@latest . -- --template react

Windows

npm create vite@latest . --- --template react

Why three dashes on Windows?

Windows npm has a known issue where two dashes (--) do not pass the template flag correctly. Using three dashes (---) is the workaround.

What does this command do?

  • npm create vite@latest runs the Vite project scaffolding tool. If this is your first time running it, npm will ask you to install the create-vite package. Type y and press Enter to confirm.
  • The . tells it to scaffold inside the current folder instead of creating a new one.
  • --template react selects the React JavaScript template.
  • Install the frontend dependencies by running:
npm install

You should see a list of added packages with no errors.

Scaffold command not working?

  • Make sure you are inside the frontend folder. Run pwd to check your current location.
  • If you see a prompt asking about the current directory not being empty, select Ignore files and continue.
  • On Windows, if you see a warning about Unknown CLI config --template, make sure you used three dashes (---) instead of two.

Help me scaffold my React project with Vite.

Verify both development servers start

Before building any features, you'll confirm both servers start and serve their default pages. The FastAPI server needs a minimal Python file to run.

  • In VS Code's Explorer sidebar, right-click the backend folder.
  • Click New File.
  • Name the file main.py and press Enter.
  • Paste the following into main.py:
from fastapi import FastAPI

# Create the FastAPI application
app = FastAPI(title="Task Manager API")


# A simple root endpoint to confirm the server is working
@app.get("/")
def root():
    return {"message": "Task Manager API"}

What does this code do?

  • FastAPI(title="Task Manager API") creates a new FastAPI application with a name that shows up in the auto-generated docs.
  • @app.get("/") defines a route that responds to GET requests at the root URL. It returns a simple JSON message to confirm the server is working.
  • Save main.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before continuing.

ⓧ I'd like to double check the full code

Here is the full main.py file. Make sure yours matches exactly.

from fastapi import FastAPI

app = FastAPI(title="Task Manager API")


@app.get("/")
def root():
    return {"message": "Task Manager API"}
  • Switch to your first terminal (the one with (venv) in the prompt, inside the backend folder).
  • Start the FastAPI development server by running:
fastapi dev main.py

What should I see?

The terminal shows a startup message with Uvicorn running on http://127.0.0.1:8000. The server keeps running in this terminal, so do not close it.

  • Open http://localhost:8000 in your browser.

You should see {"message":"Task Manager API"} displayed as a JSON response.

  • Switch to your second terminal (the one inside the frontend folder).
  • Start the React development server by running:
npm run dev
  • Open http://localhost:5173 in your browser.

You should see the default Vite React welcome page with a spinning React logo and a counter button.

Server not starting?

  • For the FastAPI server: make sure (venv) is showing in that terminal. If not, re-run the activate command.
  • For the React server: make sure you are in the frontend folder where package.json lives. Run ls to check.
  • If a port is already in use, stop any other dev servers running on ports 8000 or 5173.

Help me get my development servers running.

Both servers are running and ready to go. Next up, you'll build the Task API with FastAPI and create your first CRUD endpoints.

Build the Task API with FastAPI

Your development environment is set up and both servers are running. Now it's time to build the brain of your task manager.

Every task manager needs a way to create tasks, list them, mark them done, and delete them. You'll build a REST API with FastAPI that handles all four of these operations. These four operations are known as CRUD (Create, Read, Update, Delete).

In this step, get ready to:
  • Define data models for tasks using Pydantic.
  • Build four CRUD endpoints for creating, reading, updating, and deleting tasks.
  • Test your API using FastAPI's built-in interactive documentation.
Define the Task data models

Before you can build endpoints, the API needs to know what a task looks like. You'll define data models that describe the shape of your task data, then set up CORS middleware so your React frontend can communicate with this API.

  • Open main.py in the backend folder.
  • Select all the existing code and replace it with the following:
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from mangum import Mangum
import uuid
from datetime import datetime

# Create the FastAPI app
app = FastAPI(title="Task Manager API")

# Allow the React frontend to make requests to this API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

What does this code do?

The first six lines import everything you need. HTTPException lets you return error responses. BaseModel from Pydantic validates incoming data. Mangum is the adapter that lets FastAPI run on AWS Lambda later.

The CORSMiddleware block tells your API to accept requests from any origin. Without this, your React frontend would be blocked from talking to the API because browsers enforce a same-origin policy by default.

What is CORS?

CORS stands for Cross-Origin Resource Sharing. When your React app at localhost:5173 tries to fetch data from your API at localhost:8000, the browser sees two different origins and blocks the request by default.

Adding CORS middleware tells the API to include special headers that say "yes, I allow requests from other origins." This unlocks communication between your frontend and backend.

  • Save main.py.
  • Refresh http://localhost:8000/docs in your browser. You should see the Task Manager API title with no endpoints listed yet.

Seeing a server error or the old page?

  • Make sure you saved main.py. The FastAPI dev server auto-reloads on save, but only if the file is actually saved.
  • Check that your FastAPI dev server is still running in the terminal. If it crashed, restart it with fastapi dev main.py from the backend folder.
  • If the terminal shows a syntax error, double check that all parentheses and brackets are closed.

Help me fix my FastAPI server error after adding imports and CORS middleware.

Now that the app and middleware are set up, you need to define what a "task" actually looks like in your system.

  • Add the following code below the CORSMiddleware block in main.py:
# Input model: what the user sends when creating a task
class TaskCreate(BaseModel):
    title: str

# Full task model: what the API stores and returns
class Task(BaseModel):
    id: str
    title: str
    completed: bool
    created_at: str

# In-memory storage for tasks (resets when the server restarts)
tasks: dict[str, Task] = {}

What does this code do?

Pydantic models act as blueprints for your data. TaskCreate defines what a user sends when creating a task (just a title). Task defines the full task with an ID, title, completion status, and timestamp.

FastAPI automatically validates incoming requests against these models. If someone sends a request without a title, the API returns a clear error before your code even runs.

The tasks dictionary stores tasks in memory using their ID as the key. This is fast and simple for development. The tradeoff is that all tasks disappear when the server restarts.

  • Save main.py.

You won't see a change in the browser yet. The next chunk uses these models to create API endpoints.

Build the four CRUD endpoints

With your data models in place, you can build the endpoints that handle each CRUD operation. Each endpoint is a Python function decorated with the HTTP method it responds to.

  • Add the read and create endpoints below the tasks dictionary in main.py:
@app.get("/tasks")
def get_tasks():
    # Return all tasks as a list
    return list(tasks.values())


@app.post("/tasks", status_code=201)
def create_task(task_input: TaskCreate):
    # Generate a unique ID and timestamp for the new task
    task_id = str(uuid.uuid4())
    task = Task(
        id=task_id,
        title=task_input.title,
        completed=False,
        created_at=datetime.now().isoformat(),
    )
    # Store the task and return it
    tasks[task_id] = task
    return task

What does this code do?

  • GET /tasks returns every task in the dictionary as a list. This is the "Read" in CRUD.
  • POST /tasks creates a new task. FastAPI automatically parses the request body into a TaskCreate object, rejecting any request that doesn't include a title.
  • status_code=201 tells the API to return HTTP 201 (Created) instead of the default 200 (OK). This is a REST convention that signals a new resource was created.
  • uuid.uuid4() generates a unique ID for each task. datetime.now().isoformat() records when the task was created.
  • Save main.py.
  • Refresh http://localhost:8000/docs in your browser. You should see two new endpoints listed: GET /tasks and POST /tasks.

Don't see the endpoints?

  • Check your terminal for error messages. A missing colon or incorrect indentation will prevent the server from reloading.
  • Make sure the @app.get and @app.post decorators are directly above their function definitions with no blank lines between them.

Help me fix my GET and POST endpoints in FastAPI.

Your API can create and list tasks. Now it needs two more operations to complete the CRUD set: toggling a task's completion status and deleting a task.

  • Add the update, delete, and Lambda handler code below the create_task function in main.py:
@app.patch("/tasks/{task_id}")
def toggle_task(task_id: str):
    # Return 404 if the task doesn't exist
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    # Flip the completed status
    tasks[task_id].completed = not tasks[task_id].completed
    return tasks[task_id]


@app.delete("/tasks/{task_id}")
def delete_task(task_id: str):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    # Remove the task from storage
    del tasks[task_id]
    return {"message": "Task deleted"}


# Adapter for running FastAPI on AWS Lambda
handler = Mangum(app)

What does this code do?

  • PATCH /tasks/{task_id} toggles a task between complete and incomplete. The {task_id} in the URL is a path parameter that FastAPI automatically passes to the function.
  • DELETE /tasks/{task_id} removes a task from the dictionary entirely.
  • Both endpoints check if the task exists first. If it doesn't, they raise an HTTPException with a 404 status code. This returns a clear error to the caller instead of crashing.
  • The last line, handler = Mangum(app), wraps your FastAPI app so it can run on AWS Lambda. You won't use it locally, but it needs to be in the file for deployment later.
  • Save main.py.
  • Refresh http://localhost:8000/docs in your browser. You should now see all four endpoints listed: GET, POST, PATCH, and DELETE.

Seeing fewer than four endpoints?

  • Make sure you pasted the new code below the create_task function, not inside it. Each function should start at the same indentation level (no extra indent).
  • Check your terminal for error messages. A typo in the decorator (like @app.path instead of @app.patch) will cause a crash.

Help me fix my PATCH and DELETE endpoints in FastAPI.

That completes all the code for your API. Double check your full file matches before moving on.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.py file. Compare it against yours and fix any differences.

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from mangum import Mangum
import uuid
from datetime import datetime

app = FastAPI(title="Task Manager API")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class TaskCreate(BaseModel):
    title: str


class Task(BaseModel):
    id: str
    title: str
    completed: bool
    created_at: str


tasks: dict[str, Task] = {}


@app.get("/tasks")
def get_tasks():
    return list(tasks.values())


@app.post("/tasks", status_code=201)
def create_task(task_input: TaskCreate):
    task_id = str(uuid.uuid4())
    task = Task(
        id=task_id,
        title=task_input.title,
        completed=False,
        created_at=datetime.now().isoformat(),
    )
    tasks[task_id] = task
    return task


@app.patch("/tasks/{task_id}")
def toggle_task(task_id: str):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    tasks[task_id].completed = not tasks[task_id].completed
    return tasks[task_id]


@app.delete("/tasks/{task_id}")
def delete_task(task_id: str):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks[task_id]
    return {"message": "Task deleted"}


handler = Mangum(app)
Test the API with FastAPI's interactive docs

FastAPI automatically generates an interactive documentation page called Swagger UI. You can use it to test every endpoint directly in your browser without writing any client code.

  • Open http://localhost:8000/docs in your browser.

You should see all four endpoints listed with color-coded HTTP methods. Try each one in this order to verify the full lifecycle of a task.

  • Click the POST /tasks row to expand it.
  • Click Try it out.
  • In the request body, replace the placeholder text with:
{
  "title": "Build a task manager"
}
  • Click Execute.

You should see a response with status code 201 and a JSON body containing your task with a generated id, the title you typed, completed: false, and a created_at timestamp.

  • Create one more task by clicking Execute again with a different title (like "Deploy to AWS").
  • Expand the GET /tasks row.
  • Click Try it out, then Execute.

You should see a JSON array containing both tasks you created.

  • Copy the id value from one of your tasks in the response.
  • Expand the PATCH /tasks/{task_id} row.
  • Click Try it out.
  • Paste the task ID into the task_id field.
  • Click Execute.

The response should show the same task, but now with completed: true.

  • Expand the DELETE /tasks/{task_id} row.
  • Click Try it out.
  • Paste the same task ID into the task_id field.
  • Click Execute.

You should see {"message": "Task deleted"} in the response. Run GET /tasks one more time to confirm only one task remains.

Why test with Swagger UI?

Swagger UI is auto-generated from your code. Every endpoint, parameter, and data model is documented without you writing a single line of documentation.

Testing here before building the frontend lets you catch API bugs early. If an endpoint breaks in the browser later, you'll know the problem is in the frontend code, not the API.

Getting a 422 or 500 error?

  • A 422 error means the request body doesn't match the expected format. Make sure your POST request body is valid JSON with a title field.
  • A 500 error means something crashed in your Python code. Check your terminal for the full error traceback.
  • For PATCH and DELETE, make sure you're pasting a real task ID from a previous POST response. A random string will return a 404.

Help me debug my FastAPI endpoint errors in Swagger UI.

Your API is fully built and tested. Every endpoint creates, reads, updates, or deletes tasks exactly as expected. Next up, you'll build the React frontend that puts a real user interface on top of this API.

Build the React Frontend

Your API is up and running. You tested all four endpoints through FastAPI's interactive docs, and every one works perfectly.

React lets you build interactive UIs by breaking them into reusable components. In this step, you'll create a React frontend that sends real requests to your FastAPI backend every time a user adds, completes, or deletes a task.

In this step, get ready to:
  • Create an API helper module that connects React to your FastAPI backend.
  • Build the task manager UI with React components and hooks.
  • Style the app and test the full stack end-to-end in your browser.
Create the API helper module

Your React app needs to know where your backend lives. Vite uses environment variables prefixed with VITE_ to pass configuration into your app at build time.

  • In VS Code's Explorer sidebar, right-click the frontend folder (not frontend/src).
  • Click New File and name it .env.development.
  • Add this line to the file:
VITE_API_URL=http://localhost:8000

What does this file do?

During development, Vite reads .env.development and makes any variable starting with VITE_ available in your JavaScript code via import.meta.env. This keeps the backend URL out of your application code so you can swap it for production later.

  • Save the file.

Now you'll create the module that handles all communication between React and your API. This keeps network logic in one place instead of scattered across your components.

  • In the Explorer sidebar, right-click the frontend/src folder.
  • Click New File and name it api.js.
  • Add the following code to api.js:
// Read the backend URL from the environment variable, with a fallback
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";

// Fetch all tasks from the backend
export async function fetchTasks() {
  const response = await fetch(`${API_URL}/tasks`);
  return response.json();
}

// Send a new task title to the backend
export async function createTask(title) {
  const response = await fetch(`${API_URL}/tasks`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title }),
  });
  return response.json();
}

// Toggle a task's completed status
export async function toggleTask(taskId) {
  const response = await fetch(`${API_URL}/tasks/${taskId}`, {
    method: "PATCH",
  });
  return response.json();
}

// Delete a task by ID
export async function deleteTask(taskId) {
  await fetch(`${API_URL}/tasks/${taskId}`, {
    method: "DELETE",
  });
}

What does this code do?

This module exports four functions that match the four endpoints you built in your FastAPI backend. Each function uses the browser's built-in Fetch API to send HTTP requests.

  • fetchTasks() sends a GET request to /tasks and returns the JSON array of tasks.
  • createTask(title) sends a POST request with the task title in the request body.
  • toggleTask(taskId) sends a PATCH request to flip the task's completed status.
  • deleteTask(taskId) sends a DELETE request to remove the task.
  • Save api.js.

You won't see a change yet. The React component you build next imports these functions.

Seeing red underlines in api.js?

If VS Code shows a warning about import.meta.env, that's expected. The Vite dev server resolves this at runtime. The warning disappears once you start the dev server later.

help me fix import warnings in my Vite project

✔️ Awesome, I've got everything!

Make sure you've saved both files before moving on.

ⓧ I'd like to double check the full code

VITE_API_URL=http://localhost:8000
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000";

export async function fetchTasks() {
  const response = await fetch(`${API_URL}/tasks`);
  return response.json();
}

export async function createTask(title) {
  const response = await fetch(`${API_URL}/tasks`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title }),
  });
  return response.json();
}

export async function toggleTask(taskId) {
  const response = await fetch(`${API_URL}/tasks/${taskId}`, {
    method: "PATCH",
  });
  return response.json();
}

export async function deleteTask(taskId) {
  await fetch(`${API_URL}/tasks/${taskId}`, {
    method: "DELETE",
  });
}
Build the task manager UI in App.jsx

React components are JavaScript functions that return JSX, a syntax that looks like HTML but compiles to JavaScript. You'll use React's built-in hooks to manage your task data and respond to user actions.

  • Open frontend/src/App.jsx in VS Code.
  • Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Paste the following code to set up the component's imports, state, and data loading:
import { useState, useEffect } from "react";
import { fetchTasks, createTask, toggleTask, deleteTask } from "./api";
import "./App.css";

function App() {
  // Store the list of tasks, the text input value, and loading state
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState("");
  const [loading, setLoading] = useState(true);

  // Load tasks from the API when the component first renders
  useEffect(() => {
    loadTasks();
  }, []);

  // Fetch tasks from the backend and update state
  async function loadTasks() {
    try {
      const data = await fetchTasks();
      setTasks(data);
    } catch (error) {
      console.error("Failed to load tasks:", error);
    } finally {
      setLoading(false);
    }
  }

What are useState and useEffect?

useState creates a piece of data (called "state") that React watches. When state changes, React automatically re-renders the component to show the updated data.

useEffect runs code when the component first appears on the page. The empty array [] tells React to run this effect only once, not on every re-render.

You won't see a change yet. The next chunk adds the functions that handle user actions.

  • Add the following three handler functions directly below the loadTasks function (still inside the App component):
  // Handle the form submission to add a new task
  async function handleAddTask(e) {
    e.preventDefault();
    if (!newTask.trim()) return;
    try {
      const task = await createTask(newTask.trim());
      setTasks([...tasks, task]);
      setNewTask("");
    } catch (error) {
      console.error("Failed to create task:", error);
    }
  }

  // Toggle a task's completed status when clicked
  async function handleToggleTask(taskId) {
    try {
      const updated = await toggleTask(taskId);
      setTasks(tasks.map((t) => (t.id === taskId ? updated : t)));
    } catch (error) {
      console.error("Failed to toggle task:", error);
    }
  }

  // Remove a task from the list
  async function handleDeleteTask(taskId) {
    try {
      await deleteTask(taskId);
      setTasks(tasks.filter((t) => t.id !== taskId));
    } catch (error) {
      console.error("Failed to delete task:", error);
    }
  }

What do these handlers do?

  • handleAddTask prevents the default form reload, sends the new task to your API, then adds the returned task to the tasks array so it appears instantly.
  • handleToggleTask calls the PATCH endpoint and swaps the old task object with the updated one returned by the API.
  • handleDeleteTask calls the DELETE endpoint and filters the deleted task out of the array.

You won't see a change yet. The final chunk adds the JSX that renders the UI.

  • Add the following code directly below the handleDeleteTask function (still inside the App component). This is the JSX return that renders the page:
  // Show a loading message while tasks are being fetched
  if (loading) return <p className="loading">Loading tasks...</p>;

  return (
    <div className="app">
      <h1>Task Manager</h1>
      {/* Form for adding new tasks */}
      <form onSubmit={handleAddTask} className="add-form">
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit">Add</button>
      </form>
      {/* Show empty message or the task list */}
      {tasks.length === 0 ? (
        <p className="empty">No tasks yet. Add one above!</p>
      ) : (
        <ul className="task-list">
          {tasks.map((task) => (
            <li key={task.id} className={task.completed ? "completed" : ""}>
              <span
                onClick={() => handleToggleTask(task.id)}
                className="task-title"
              >
                {task.title}
              </span>
              <button
                onClick={() => handleDeleteTask(task.id)}
                className="delete-btn"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;

What does this JSX do?

The return block describes what the page looks like. React reads this JSX and turns it into real HTML in the browser.

  • The <form> captures the text input and calls handleAddTask when submitted.
  • tasks.map() loops through every task and renders a list item for each one.
  • Clicking a task title calls handleToggleTask. Clicking the Delete button calls handleDeleteTask.
  • Save App.jsx.

The page won't look right yet because the CSS still has Vite's default styles. You'll replace those next.

Seeing a blank page or error in the browser?

Check that all three code chunks are inside the App function, not outside it. The handlers and JSX must be between function App() { and the final closing }.

Also make sure export default App; appears at the very bottom of the file, outside the function.

help me debug my App.jsx component

✔️ Awesome, I've got everything!

Make sure you've saved the file before moving on to the CSS.

ⓧ I'd like to double check the full code

import { useState, useEffect } from "react";
import { fetchTasks, createTask, toggleTask, deleteTask } from "./api";
import "./App.css";

function App() {
  const [tasks, setTasks] = useState([]);
  const [newTask, setNewTask] = useState("");
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadTasks();
  }, []);

  async function loadTasks() {
    try {
      const data = await fetchTasks();
      setTasks(data);
    } catch (error) {
      console.error("Failed to load tasks:", error);
    } finally {
      setLoading(false);
    }
  }

  async function handleAddTask(e) {
    e.preventDefault();
    if (!newTask.trim()) return;
    try {
      const task = await createTask(newTask.trim());
      setTasks([...tasks, task]);
      setNewTask("");
    } catch (error) {
      console.error("Failed to create task:", error);
    }
  }

  async function handleToggleTask(taskId) {
    try {
      const updated = await toggleTask(taskId);
      setTasks(tasks.map((t) => (t.id === taskId ? updated : t)));
    } catch (error) {
      console.error("Failed to toggle task:", error);
    }
  }

  async function handleDeleteTask(taskId) {
    try {
      await deleteTask(taskId);
      setTasks(tasks.filter((t) => t.id !== taskId));
    } catch (error) {
      console.error("Failed to delete task:", error);
    }
  }

  if (loading) return <p className="loading">Loading tasks...</p>;

  return (
    <div className="app">
      <h1>Task Manager</h1>
      <form onSubmit={handleAddTask} className="add-form">
        <input
          type="text"
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Add a new task..."
        />
        <button type="submit">Add</button>
      </form>
      {tasks.length === 0 ? (
        <p className="empty">No tasks yet. Add one above!</p>
      ) : (
        <ul className="task-list">
          {tasks.map((task) => (
            <li key={task.id} className={task.completed ? "completed" : ""}>
              <span
                onClick={() => handleToggleTask(task.id)}
                className="task-title"
              >
                {task.title}
              </span>
              <button
                onClick={() => handleDeleteTask(task.id)}
                className="delete-btn"
              >
                Delete
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default App;
Style the app and test the full stack locally

The default Vite styles don't match your task manager UI. You'll replace them with custom CSS that styles the form, task list, and buttons.

  • Open frontend/src/App.css in VS Code.
  • Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows) and delete it.
  • Paste the following styles for the app container and the add-task form:
/* Center the app and set a comfortable max width */
.app {
  max-width: 600px;
  margin: 40px auto;
  padding: 20px;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #1a1a2e;
}

/* Layout the input and button side by side */
.add-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

.add-form input {
  flex: 1;
  padding: 10px 14px;
  border: 2px solid #e0e0e0;
  border-radius: 6px;
  font-size: 16px;
}

.add-form input:focus {
  outline: none;
  border-color: #4361ee;
}

.add-form button {
  padding: 10px 20px;
  background: #4361ee;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;
}

.add-form button:hover {
  background: #3a56d4;
}

What do these styles do?

  • .app centers the entire component on the page with a 600px max width.
  • .add-form uses flexbox to place the text input and the Add button side by side.
  • The :focus and :hover states give visual feedback when the learner interacts with the form.

You won't see the final result yet. The task list still needs its own styles.

  • Add the following styles directly below the form styles you just pasted:
/* Remove default list bullets */
.task-list {
  list-style: none;
  padding: 0;
}

/* Style each task as a card-like row */
.task-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  margin-bottom: 8px;
  background: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #e0e0e0;
}

/* Make the task title clickable */
.task-title {
  cursor: pointer;
  flex: 1;
}

/* Strike through completed tasks */
.task-list li.completed .task-title {
  text-decoration: line-through;
  color: #999;
}

/* Style the delete button in red */
.delete-btn {
  background: #e63946;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 14px;
}

.delete-btn:hover {
  background: #c1121f;
}

/* Center the loading and empty-state text */
.loading,
.empty {
  text-align: center;
  color: #666;
}

What do these styles do?

  • .task-list li turns each task into a card-like row with the title on the left and the delete button on the right.
  • .completed .task-title adds a strikethrough effect when a task is marked complete.
  • .delete-btn gives the delete button a red background that darkens on hover.
  • Save App.css.

Styles not applying?

Make sure the top of your App.jsx includes import "./App.css";. Without this import, React doesn't know about the stylesheet.

Also confirm you deleted all of Vite's default CSS before pasting. Leftover styles can override yours.

help me fix CSS not loading in my React app

✔️ Awesome, I've got everything!

Make sure you've saved the file. Time to test the full stack.

ⓧ I'd like to double check the full code

.app {
  max-width: 600px;
  margin: 40px auto;
  padding: 20px;
  font-family: system-ui, -apple-system, sans-serif;
}

h1 {
  text-align: center;
  color: #1a1a2e;
}

.add-form {
  display: flex;
  gap: 8px;
  margin-bottom: 24px;
}

.add-form input {
  flex: 1;
  padding: 10px 14px;
  border: 2px solid #e0e0e0;
  border-radius: 6px;
  font-size: 16px;
}

.add-form input:focus {
  outline: none;
  border-color: #4361ee;
}

.add-form button {
  padding: 10px 20px;
  background: #4361ee;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;
}

.add-form button:hover {
  background: #3a56d4;
}

.task-list {
  list-style: none;
  padding: 0;
}

.task-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  margin-bottom: 8px;
  background: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #e0e0e0;
}

.task-title {
  cursor: pointer;
  flex: 1;
}

.task-list li.completed .task-title {
  text-decoration: line-through;
  color: #999;
}

.delete-btn {
  background: #e63946;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 14px;
}

.delete-btn:hover {
  background: #c1121f;
}

.loading,
.empty {
  text-align: center;
  color: #666;
}

All three frontend files are in place. Time to run both servers and see it all come together.

  • Make sure your FastAPI backend is still running. If you stopped it, start it again by navigating to your backend folder and running:
fastapi dev main.py
  • Open a second terminal window. You need both servers running at the same time.
  • Navigate to your frontend folder and start the React dev server by running:
npm run dev
  • Open http://localhost:5173 in your browser.

You should see the Task Manager heading, a text input with an Add button, and the message "No tasks yet. Add one above!" below it.

  • Type Buy groceries into the input field and click Add.
  • Click the task title to toggle it as complete. You should see a strikethrough effect.
  • Click the Delete button to remove the task.

Tasks not appearing after you click Add?

Open your browser's developer console with F12 and check for errors. The most common causes:

  • CORS error in the console means your backend isn't running. Make sure fastapi dev main.py is active in your other terminal.
  • Network error or Failed to fetch means the VITE_API_URL value is wrong. Check that .env.development contains VITE_API_URL=http://localhost:8000 with no quotes around the URL.
  • If you just created .env.development, you may need to restart the Vite dev server. Press Ctrl+C in the frontend terminal and run npm run dev again.

help me debug my React app connecting to FastAPI

Your full-stack app is running locally. The React frontend sends requests to your FastAPI backend, and every create, toggle, and delete works in real time. Next up, you'll deploy the backend to AWS Lambda so it's live on the internet.

Deploy the Backend to AWS Lambda

Your Task Manager works perfectly on localhost. The next challenge is making your API accessible to anyone on the internet.

AWS Lambda is a serverless compute service. You give it a zip file containing your code, and it runs that code whenever an HTTP request arrives. In this step, you'll package your FastAPI app, upload it to Lambda, and connect it to a public URL through API Gateway.

In this step, get ready to:
  • Package the FastAPI app and its dependencies into a deployment zip for AWS Lambda.
  • Create a Lambda function in the AWS Console and upload the deployment package.
  • Add an API Gateway trigger and test the live API endpoint.
Create the deployment package

Lambda needs all your code and dependencies bundled into a single zip file. Your main.py already includes the Mangum handler at the bottom, and your requirements.txt lists the two dependencies. All that's left is packaging.

Lambda runs on Linux. Even if your own machine runs macOS or Windows, you need to install Linux-compatible versions of your Python dependencies.

  • Stop the FastAPI dev server if it's still running by pressing Ctrl+C in your terminal.
  • Navigate to the backend folder inside your task-manager project by running:
cd ~/Desktop/task-manager/backend
  • Install the Python dependencies into a package folder targeting the Linux platform by running:
pip install --platform manylinux2014_x86_64 --target ./package --only-binary=:all: -r requirements.txt

What does this command do?

  • --platform manylinux2014_x86_64 downloads packages built for Linux, which is what Lambda runs.
  • --target ./package installs everything into a package folder instead of your system Python.
  • --only-binary=:all: ensures pip only downloads pre-built binary packages, avoiding compilation issues across platforms.
  • Confirm the package folder was created by running:
ls package

You should see folders like fastapi, pydantic, mangum, and several others listed in the output.

Seeing an error from pip?

  • Make sure you're inside the backend folder where requirements.txt lives.
  • If you see ERROR: No matching distribution found, double-check that --only-binary=:all: is included in the command.
  • Why is my pip install failing when packaging for Lambda?
  • Copy main.py into the package folder and zip everything into deployment.zip by running these commands:

macOS

cp main.py package/
cd package
zip -r ../deployment.zip .
cd ..

Windows

copy main.py package\
cd package
Compress-Archive -Path * -DestinationPath ..\deployment.zip
cd ..

Why does main.py go into the package folder?

Lambda expects your code and all dependencies at the root level of the zip file. By copying main.py into the package folder before zipping, everything ends up at the root of the archive.

  • Confirm deployment.zip was created by running:
ls ../deployment.zip

You should see deployment.zip listed in the output. This single file contains your entire backend, ready for Lambda.

Don't see deployment.zip?

  • Make sure you ran cd package before the zip command, and cd .. afterwards to return to the backend folder.
  • On Windows, if Compress-Archive is not recognized, make sure you're using PowerShell, not Command Prompt.
  • Why can't I create the deployment zip for Lambda?
Create the Lambda function in the AWS Console

With your deployment package ready, you can create the Lambda function that will run your API in the cloud.

  • Open your browser and go to console.aws.amazon.com.
  • Sign in with your AWS account credentials.
  • In the search bar at the top of the console, type Lambda.
  • Click Lambda in the search results to open the Lambda dashboard.
  • Click Create function.
  • Make sure Author from scratch is selected at the top.
  • Enter task-manager-api in the Function name field.
  • Under Runtime, select Python 3.12.
  • Click Create function.

AWS creates your function and takes you to the function's configuration page.

  • In the Code tab, click the Upload from dropdown.
  • Select .zip file.
  • Click Upload and select deployment.zip from the backend folder on your Desktop.
  • Click Save.

Lambda's default handler expects a file called lambda_function.py, but your app lives in main.py. You need to update the handler setting to point to the right file.

  • Scroll down to the Runtime settings section.
  • Click Edit.
  • Replace the default value in the Handler field with main.handler.
  • Click Save.

What is a Lambda handler?

The handler tells Lambda which function to call when a request comes in. main.handler means "look in the file main.py and call the function named handler."

That handler function is the Mangum adapter you already have at the bottom of main.py. Mangum translates Lambda events into standard HTTP requests that FastAPI understands.

Lambda's default timeout is 3 seconds. FastAPI needs more time to start up on its first request (called a "cold start"), so you need to increase it.

  • Click the Configuration tab at the top of the function page.
  • Click General configuration in the left sidebar.
  • Click Edit.
  • Set the Timeout to 0 minutes and 30 seconds.
  • Click Save.

Function page looks different?

AWS Console layouts can vary by region and account type. Look for tabs labeled Code, Test, Monitor, and Configuration near the top of the page.

I can't find the runtime settings or configuration tab in my Lambda function page.

Add an API Gateway trigger and test

Your Lambda function exists, but it doesn't have a public URL yet. API Gateway creates one and forwards incoming HTTP requests to your Lambda function.

  • Scroll back to the top of your Lambda function page.
  • In the Function overview section, click Add trigger.
  • Select API Gateway from the trigger dropdown.
  • Select Create a new API.
  • Choose HTTP API as the API type.
  • Under Security, select Open.
  • Click Add.

What is API Gateway?

API Gateway is the front door for your Lambda function. It receives HTTP requests from the internet and forwards them to Lambda.

The HTTP API type is lightweight and fits within the AWS free tier (1 million calls per month for 12 months).

After adding the trigger, you'll see an API endpoint URL in the trigger details.

  • Find the API endpoint URL in the Triggers section at the bottom of the Function overview.
  • Copy the full URL.
  • Open a new browser tab and paste the URL, then add /tasks to the end.

The full URL should look something like this:

https://abc123xyz.execute-api.us-east-1.amazonaws.com/tasks

You should see an empty JSON array [] in your browser. This confirms your FastAPI app is running on Lambda and API Gateway is forwarding requests to it.

Seeing an error instead of []?

  • If you see {"message":"Internal Server Error"}, check that the handler is set to main.handler in Runtime settings (not the default lambda_function.lambda_handler).
  • If you see a 403 Forbidden error, make sure you selected Open for the API Gateway security setting.
  • If the page times out, check that the timeout is set to 30 seconds in the Lambda configuration.
  • My Lambda function is returning an error when I visit the API Gateway URL.

Save your API endpoint URL

Keep this API Gateway URL handy. You'll need it in the next step when you connect the React frontend to this live backend.

Your backend API is now live on the internet. Next up, you'll deploy the React frontend to AWS Amplify and connect it to this endpoint.

Deploy the Frontend to AWS Amplify

Your backend API is live on AWS, responding to requests from anywhere in the world. Now it needs a frontend that people can actually visit.

AWS Amplify is a hosting service that connects directly to your GitHub repository. Every time you push code, Amplify automatically builds and deploys your React app.

In this step, get ready to:
  • Create a production environment file and push your code to GitHub.
  • Connect your GitHub repository to AWS Amplify.
  • Deploy and verify the live full-stack app.
Update the API URL and push to GitHub

Your React frontend reads the backend URL from an environment variable called VITE_API_URL. Right now, .env.development points to http://localhost:8000 for local work.

For the live deployment, you need a production version that points to your API Gateway URL.

  • In VS Code, create a new file called .env.production inside the frontend folder (right next to your existing .env.development file).
  • Add the following line:
VITE_API_URL=[[API_GATEWAY_URL="your API Gateway endpoint URL"]]

What does this file do?

Vite loads environment variables from different files depending on the mode. When you run npm run dev, Vite reads from .env.development. When you run npm run build for production, it reads from .env.production.

This means your app automatically talks to localhost during development and your live API Gateway URL in production.

  • Save .env.production.

Next, you need a .gitignore file to tell Git which files should stay out of your repository. Things like installed packages, environment files, and build artifacts should not be committed.

  • Create a new file called .gitignore in the root of your task-manager folder (not inside frontend or backend).
  • Add the following content:
# Dependencies
node_modules/

# Environment files
.env*

# Python
__pycache__/
venv/

# Build artifacts
backend/package/
backend/deployment.zip

What does this file do?

Each line tells Git to ignore specific files or folders.

  • node_modules/ contains thousands of installed dependency files that anyone can regenerate with npm install.
  • .env* matches all environment files, keeping your API URLs and any future secrets out of the repository.
  • backend/package/ and backend/deployment.zip are Lambda deployment artifacts that are too large for a Git repository.
  • Save .gitignore.

Now you need a GitHub repository to push your code to. Amplify will connect to this repository to build and deploy your frontend.

  • Open your browser and go to github.com.
  • Click the + icon in the top-right corner of the page.
  • Select New repository from the dropdown.
  • Enter task-manager as the repository name.
  • Select Public for the repository visibility.
  • Do NOT check any of the initialization boxes (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows a Quick Setup page with your repository URL. Keep this page open because you will need the URL in a moment.

  • In your terminal, make sure you are inside the task-manager folder (the project root) by running:
cd ~/Desktop/task-manager
  • Initialize a Git repository, commit your code, and push it to GitHub by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[GITHUB_REPO_URL="https://github.com/YOUR-USERNAME/task-manager.git"]]
git push -u origin main

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all files (except those in .gitignore) for committing.
  • git branch -M main renames the default branch to main to match GitHub's default.
  • git remote add origin connects your local repository to the GitHub repository you just created.
  • git push -u origin main uploads your code to GitHub.
  • Refresh your GitHub repository page in the browser.
  • You should see your backend and frontend folders along with the .gitignore file listed in the repository.

Push failed or permission denied?

If you see a "permission denied" or authentication error, make sure you are signed into GitHub in your terminal. GitHub requires either an SSH key or a personal access token for authentication.

If you see "remote origin already exists", run git remote set-url origin <your-url> instead of git remote add origin.

Help me fix my Git push to GitHub

Connect the repository to AWS Amplify

With your code on GitHub, you can now connect it to AWS Amplify for automatic deployment. Amplify will pull your code, run the build, and host the result on a public URL.

  • In the AWS Management Console, search for Amplify in the search bar at the top.
  • Click AWS Amplify to open the Amplify console.
  • Click Create new app.
  • Under Deploy your app, select GitHub and click Next.

GitHub authorization prompt

If this is your first time connecting GitHub to Amplify, a new page opens on GitHub asking you to authorize AWS Amplify. Click Authorize, then install the Amplify GitHub App when prompted. You can choose All repositories or select only your task-manager repository.

  • Select the task-manager repository from the list.
  • Select the main branch.
  • Click Next.

Amplify auto-detects your Vite project and fills in the build settings. However, because your frontend code lives in a frontend subfolder, you need to tell Amplify where to find it.

  • On the App settings page, look for the Build settings section.
  • Confirm the Build output directory is set to dist.
  • Set the App root (sometimes labeled Monorepo root directory) to frontend.

Why set the app root?

Your GitHub repository contains both backend and frontend folders. Amplify needs to know that the React app lives inside frontend so it runs npm install and npm run build in the correct directory.

Now you need to add the production API URL as an environment variable in Amplify. This is how Amplify knows where your backend lives when it builds the app.

  • Still on the App settings page, scroll down to the Environment variables section.
  • Click Add environment variable.
  • Enter VITE_API_URL as the variable name.
  • Paste your API Gateway endpoint URL as the value (the same URL you used in .env.production).
  • Click Next.
  • Review your settings, then click Save and deploy.

Why add the variable in Amplify too?

Your .env.production file is excluded by .gitignore, so Amplify never sees it. Setting VITE_API_URL directly in Amplify's console injects the variable into the build environment. Vite picks it up during npm run build and bakes it into the production JavaScript bundle.

Don't see your repository in the list?

Make sure you completed the GitHub authorization step. If you selected Only select repositories when installing the Amplify GitHub App, check that task-manager is in the selected list.

Help me connect my GitHub repository to AWS Amplify

Deploy and verify the live app

Amplify is now building your React app from the source code on GitHub. This process takes a few minutes.

  • Wait for the build to complete. You can watch the progress in the Amplify console. The status changes from Provision to Build to Deploy to Verify.
  • Once the deployment shows a green checkmark, click Visit deployed URL (or click the URL shown under your app's domain).

How long does the build take?

The first build typically takes 2 to 5 minutes. Amplify installs your npm dependencies, runs npm run build, and deploys the output to a global CDN. Future builds are faster because dependencies are cached.

Your task manager is now live on the internet. Time to test it.

  • On the live page, type a task name (like Learn React) in the input field and click Add.
  • Click the task text to toggle it as completed. The text should get a strikethrough line.
  • Click Delete to remove the task.

If all three actions work, your full-stack app is fully deployed. The React frontend on Amplify is talking to your FastAPI backend on Lambda through API Gateway.

Tasks not showing up or getting errors?

Open your browser's developer console (F12 or Cmd+Option+I on macOS) and check the Console tab for error messages.

  • If you see a CORS error, check that your FastAPI backend has allow_origins=["*"] in the CORS middleware configuration.
  • If you see Failed to fetch or a network error, the VITE_API_URL environment variable may be wrong. Double-check that the value in Amplify matches your API Gateway URL exactly.
  • If the page loads but shows "Loading tasks..." forever, your Lambda function may be experiencing a cold start. Wait 10 seconds and refresh.

Help me debug my deployed task manager app

Save this URL

The Amplify URL (something like main.d1abc2def3.amplifyapp.com) is the live URL for your project. This is what you put on your resume to show employers a working full-stack application.

Your full-stack task manager is live. Every piece of the stack is working together: React on Amplify, FastAPI on Lambda, and API Gateway connecting them. Next up, a Secret Mission to make your tasks persist forever with DynamoDB.

Secret mission

Add Persistent Storage with DynamoDB

Right now, your tasks vanish every time the Lambda function goes cold. In this Secret Mission, replace the in-memory storage with Amazon DynamoDB so your data persists between deployments. This is what separates a demo from a production-ready application.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. All AWS resources in this project fall within the free tier for low traffic, but some free tier allowances expire after 12 months.

Cost warning

Your API Gateway HTTP API free tier (1M calls/month) and AWS Amplify free tier (1,000 build minutes, 15 GB served, 5 GB storage) both expire after 12 months.

AWS Lambda and DynamoDB (Secret Mission) are part of the Always Free tier with no expiration. If you plan to leave resources running beyond 12 months, set up a billing alarm in the AWS Console to avoid surprise charges.

Resources you used:

  • AWS Lambda function (free tier: 1M requests/month, 400K GB-seconds/month. Always free, no expiration).
  • API Gateway HTTP API (free tier: 1M calls/month for 12 months).
  • AWS Amplify app (free tier: 1,000 build minutes/month, 15 GB served/month, 5 GB storage for 12 months).
  • DynamoDB table (Secret Mission only. Free tier: 25 GB storage, 25 WCU/RCU. Always free, no expiration).
  • GitHub repository (free for public and private repos).
  • Local project files (the task-manager folder on your Desktop).

Keep everything running

No action needed. Choose this if you are still actively building or want your task manager live at its public URL.

  • All resources stay within the AWS free tier for low traffic.
  • Set up a billing alarm in the AWS Billing Console so you are notified if usage exceeds the free tier.
  • Remember that the API Gateway and Amplify free tiers expire after 12 months.

Pause - I'll come back to this later

Delete the Amplify app to stop frontend hosting while keeping your Lambda function and API Gateway running. This stops serving traffic but preserves your backend.

  • In the AWS Console, navigate to AWS Amplify.
  • Select your task manager app, then click Actions and choose Delete app.
  • Your Lambda function and API Gateway will remain available. You can redeploy the frontend later by reconnecting your GitHub repository to a new Amplify app.
  • Keep your local task-manager folder and GitHub repository so you can pick up where you left off.

Delete - I don't want to use this again

Remove all project resources to ensure zero ongoing charges.

  • In the AWS Console, navigate to AWS Amplify. Select your app, click Actions, and choose Delete app.
  • Navigate to AWS Lambda. Select your function, click Actions, and choose Delete function.
  • Navigate to API Gateway. Select the HTTP API created by your Lambda trigger, then click Delete.
  • If you completed the Secret Mission, navigate to DynamoDB. Select the tasks table and click Delete table.
  • Optionally delete your GitHub repository from the repository's Settings page.
  • Delete the task-manager folder from your Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built and deployed a full-stack web application from scratch.

That is a real thing, live on the internet, with your name on it.

You've learned how to:

  • Build a React frontend with components, state management using hooks, and API integration.
  • Create a FastAPI REST API with full CRUD operations, Pydantic validation, and automatic interactive documentation.
  • Deploy a Python backend to AWS Lambda with API Gateway, and ship a React app to AWS Amplify with CI/CD from GitHub.
  • Added persistent data storage with Amazon DynamoDB so your tasks survive between deployments.

Ready to quiz yourself?