Build a Task Tracker REST API

Build a fully tested REST API with FastAPI, Pydantic, and pytest.

Introduction

30 Second Summary

Every app you use daily (to-do lists, calendars, email) stores and retrieves data through APIs behind the scenes. Building one yourself is one of the fastest ways to prove you can write production-quality backend code.

In this project, you will build a Task Tracker REST API using Python and FastAPI that handles creating, reading, updating, and deleting tasks with automatic data validation and interactive documentation.

What You'll Build

A Task Tracker API with five endpoints that validates incoming data, returns proper HTTP status codes, and generates interactive documentation you can explore in your browser.

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

  • A FastAPI application with full CRUD operations (Create, Read, Update, Delete) for managing tasks.
  • Pydantic models that automatically validate incoming data and reject bad requests.
  • An automated test suite using pytest that verifies every endpoint works correctly.
  • Secret Mission: Add priority levels and filtering to your API.

Are there any prerequisites?

None. This guide includes setup for all required tools, including Python, VS Code, and Git. Basic familiarity with Python syntax is helpful but not required.

Set Up Your Project

Every Python project starts with the right tools and a clean workspace. Before writing any API code, you need Python itself, a code editor, and a structured project folder.

In this step, you will install Python 3.13 and VS Code, create a dedicated project folder with a virtual environment, and set up version control with Git.

In this step, get ready to:
  • Install Python 3.13, VS Code, and verify Git.
  • Create the project folder with a virtual environment and install dependencies.
  • Initialize a Git repository and make your first commit.
Install Python 3.13 and VS Code

Python is the language your API runs on. VS Code is the code editor where you will write and manage all your project files.

  • Check if Python 3.13 is already installed by opening your terminal.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.13 or higher

Your Python version is ready to go. Move on to the VS Code install below.

ⓧ I see an older version

macOS often ships with an older Python version (like 3.9) that shadows newer installs. You need Python 3.13 for this project.

  • Download the Python 3.13 macOS installer from python.org/downloads.
  • Open the downloaded .pkg file and follow the installer prompts, accepting all defaults.
  • After installation completes, close your terminal and open a new one.
  • Run python3 --version again to confirm it shows 3.13.x.

Still seeing the old version?

The python.org installer places the new binary at /Library/Frameworks/Python.framework/Versions/3.13/bin/python3. Try running python3.13 --version to confirm it installed, then use python3.13 in place of python3 throughout this project.

ⓧ Command not found

Python is not installed on your machine yet.

  • Download the Python 3.13 macOS installer from python.org/downloads.
  • Open the downloaded .pkg file and follow the installer prompts, accepting all defaults.
  • After installation completes, close your terminal and open a new one.
  • Run python3 --version to confirm it shows 3.13.x.

Now install VS Code, the editor you will use throughout this project.

  • Download VS Code from code.visualstudio.com.
  • Open the downloaded .dmg file and drag the VS Code icon into your Applications folder.
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type Shell Command: Install 'code' command in PATH and select it from the dropdown.

Why set up the code command?

This lets you open any folder in VS Code directly from the terminal by typing code . instead of navigating through menus.

Finally, verify that Git is available on your machine.

  • Switch back to your terminal and check for Git by running this command:
git --version

✔️ I see a version number

Git is ready. You are all set to move on.

ⓧ Command not found

macOS includes Git with Xcode Command Line Tools. Install them now.

  • Install the command line tools by running:
xcode-select --install
  • Follow the prompts in the dialog that appears, then wait for the installation to finish.
  • Run git --version again to confirm Git is now available.
Create the project and virtual environment

A virtual environment gives your project its own isolated set of Python packages. This prevents conflicts between different projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir task-tracker-api
cd task-tracker-api
  • Create a virtual environment and activate it by running these commands:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python3 -m venv venv
venv\Scripts\activate

How do I know the virtual environment is active?

Your terminal prompt now starts with (venv). This confirms that any packages you install will stay inside this project folder.

  • Open the project in VS Code by running this command:
code .

VS Code opens with your task-tracker-api folder in the Explorer sidebar.

  • Create a new file in VS Code by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it requirements.txt.
  • Paste the following content into requirements.txt:
fastapi[standard]==0.138.2
pytest==9.1.1

What does this file do?

A requirements.txt file lists the exact package versions your project needs. FastAPI is the web framework for building your API. pytest is the testing framework you will use later to verify your endpoints work correctly.

Pinning versions (the == syntax) ensures anyone who runs your project gets the same packages you used.

  • Save requirements.txt.
  • Install the dependencies by running this command in your VS Code terminal (open it with Ctrl+` or Terminal > New Terminal from the menu):
pip install -r requirements.txt

You should see pip downloading and installing packages. Once it finishes, verify both tools are available.

  • Confirm FastAPI installed by running:
fastapi --version
  • Confirm pytest installed by running:
pytest --version

Both commands should return version information without errors.

Seeing 'command not found' for fastapi or pytest?

Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate (macOS) or venv\Scripts\activate (Windows) again.

If the virtual environment is active but the commands still fail, re-run pip install -r requirements.txt and check for error messages during installation.

help me troubleshoot why fastapi or pytest commands are not found after installing requirements

✔️ Awesome, I've got everything!

Great. Double check you have saved requirements.txt.

ⓧ I'd like to double check the full code

Here is the full requirements.txt for comparison:

fastapi[standard]==0.138.2
pytest==9.1.1
Initialize Git and create the project files

Version control tracks every change you make to your code. Employers expect to see meaningful commit history in your projects.

  • Initialize a Git repository in your project folder by running this command:
git init

You should see a message confirming the repository was initialized.

  • Create a new file in VS Code and save it as .gitignore.
  • Paste the following content into .gitignore:
venv/
__pycache__/
.pytest_cache/

What does .gitignore do?

This file tells Git which files and folders to ignore. The venv/ folder contains thousands of package files that do not belong in your repository. __pycache__/ and .pytest_cache/ hold temporary files Python and pytest generate at runtime.

  • Save .gitignore.

✔️ Awesome, I've got everything!

Great. Double check you have saved .gitignore.

ⓧ I'd like to double check the full code

Here is the full .gitignore for comparison:

venv/
__pycache__/
.pytest_cache/
  • Create the empty project files by running this command:
touch main.py models.py test_main.py
  • Confirm the files exist by running:
ls

You should see main.py, models.py, test_main.py, requirements.txt, .gitignore, and venv/ in the output.

  • Stage all files and make your first commit by running these commands:
git add .
git commit -m "Initial project setup"

You should see a commit summary listing the files that were added.

Git asking you to set your identity?

If Git asks for your name and email before committing, run these two commands (replacing the placeholder values with your own):

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

Then run the git commit command again.

help me configure Git for the first time on my machine

Your workspace is ready. Next up, you will define your data models and build the first working API endpoint.

Define Data Models and Create Your First Endpoint

Your development environment is ready with Python, FastAPI, and a clean project structure.

Now it's time to write actual code. Every API needs to define the shape of its data and then create endpoints that accept and return that data. In this step, you'll define your task models and bring the API to life.

In this step, get ready to:
  • Define Pydantic models that validate incoming data automatically.
  • Create your first API endpoints (a root welcome and a task creator).
  • Launch the development server and test your API in the browser.
Define Pydantic models in models.py

Your API needs to know what a "task" looks like before it can accept or return one. Pydantic models define the exact fields and types each task must have. FastAPI uses these models to automatically reject any request that doesn't match the expected shape.

  • Open models.py in VS Code and add the following code:
from typing import Union

from pydantic import BaseModel


# Schema for incoming task data (what the client sends)
class TaskCreate(BaseModel):
    title: str
    description: Union[str, None] = None
    completed: bool = False


# Full task model with an ID (what the API returns)
class Task(TaskCreate):
    id: int

What does this code do?

You're defining two models that describe the shape of a task:

  • TaskCreate is the schema for incoming data. It requires a title (string), allows an optional description, and defaults completed to False.
  • Task extends TaskCreate and adds an id field. This is what your API returns to the client after assigning an ID.

By separating the two, incoming requests never need to provide an ID. Your API assigns one automatically.

  • Save models.py.
  • You won't see a visible change yet. The next substep imports these models into main.py.

Seeing a red underline in VS Code?

If VS Code shows a warning on the from pydantic import BaseModel line, it may not be using your virtual environment's Python interpreter. Press Cmd+Shift+P, type "Python: Select Interpreter", and choose the one inside your venv folder.

help me fix import errors in VS Code

✔️ Awesome, I've got everything!

Great. Make sure you've saved models.py before moving on.

ⓧ I'd like to double check the full code

from typing import Union

from pydantic import BaseModel


class TaskCreate(BaseModel):
    title: str
    description: Union[str, None] = None
    completed: bool = False


class Task(TaskCreate):
    id: int
Build the FastAPI app in main.py

With your data models defined, you can now create the application that receives HTTP requests and responds with task data.

  • Open main.py in VS Code and add the following code:
from fastapi import FastAPI, HTTPException

from models import Task, TaskCreate

# Create the FastAPI application instance
app = FastAPI()

# In-memory storage for tasks
tasks: list[Task] = []
# Counter to assign unique IDs to new tasks
task_id_counter: int = 1


# Root endpoint - a simple welcome message
@app.get("/")
async def root():
    return {"message": "Task Tracker API"}


# Create a new task from incoming JSON data
@app.post("/tasks", status_code=201)
async def create_task(task_data: TaskCreate) -> Task:
    global task_id_counter
    # Build a Task with an auto-assigned ID
    task = Task(id=task_id_counter, **task_data.model_dump())
    tasks.append(task)
    task_id_counter += 1
    return task

What does this code do?

This file creates your entire API application:

  • app = FastAPI() creates the application that handles incoming requests.
  • tasks is a Python list that stores tasks in memory. When you restart the server, this list resets to empty.
  • The root endpoint (GET /) returns a welcome message. Think of it as a health check.
  • The create endpoint (POST /tasks) receives a JSON body, validates it against TaskCreate, assigns an ID, stores the task, and returns it with status code 201 (Created).

The model_dump() method converts the Pydantic model into a dictionary, and ** unpacks those key-value pairs as arguments to build the full Task object.

  • Save main.py.
  • You won't see a change yet. The next substep starts the server so you can test everything.

Red underline on the models import?

If from models import Task, TaskCreate shows an error, make sure you saved models.py first. VS Code resolves imports from saved files on disk, not from unsaved editor buffers.

help me fix my models import

✔️ Awesome, I've got everything!

Great. Make sure you've saved main.py before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException

from models import Task, TaskCreate

app = FastAPI()

tasks: list[Task] = []
task_id_counter: int = 1


@app.get("/")
async def root():
    return {"message": "Task Tracker API"}


@app.post("/tasks", status_code=201)
async def create_task(task_data: TaskCreate) -> Task:
    global task_id_counter
    task = Task(id=task_id_counter, **task_data.model_dump())
    tasks.append(task)
    task_id_counter += 1
    return task
Run the development server and test it

Your code is written. Time to see it in action by starting the FastAPI development server.

  • Start the development server by running this command in your terminal:
fastapi dev main.py

You should see output showing the server is running at http://127.0.0.1:8000.

Seeing an import error or ModuleNotFoundError?

Make sure your virtual environment is activated. Your terminal prompt should start with (venv). If it doesn't, run source venv/bin/activate and try again.

Also check that both models.py and main.py are saved. The server reads from saved files on disk.

help me fix my FastAPI server startup error

  • Open your browser and go to http://127.0.0.1:8000/docs.

This is the Swagger UI. FastAPI auto-generates this interactive documentation page from your code. You'll see your two endpoints listed with their HTTP methods and paths.

  • Click on POST /tasks to expand it.
  • Click Try it out.
  • Replace the example JSON body with this:
{
  "title": "Buy groceries",
  "description": "Milk, eggs, bread"
}
  • Click Execute.

You should see a response with status code 201 and a JSON body containing your task data plus an auto-assigned id of 1.

What is Swagger UI?

Swagger UI is an interactive documentation page that FastAPI generates automatically from your code. It reads your endpoint decorators, type annotations, and Pydantic models to build a page where you can test every endpoint directly in your browser.

No configuration needed. As long as your server is running, /docs is always available.

  • Now test validation. Click Try it out again and send this invalid body (missing the required title field):
{
  "description": "No title provided"
}
  • Click Execute.

You should see a 422 validation error response. Pydantic rejected the request because title is required. You didn't write any validation logic. Pydantic handles it automatically based on your model definition.

  • Stop the server by pressing Ctrl+C in your terminal.
  • Commit your progress by running these commands:
git add .
git commit -m "Add data models and first endpoints"

Your first two endpoints are live and validated. Next up, you'll add the remaining endpoints to build a full CRUD API.

Build the Full CRUD API

Your API can create tasks, but users have no way to retrieve, update, or delete them. A production API needs all four CRUD operations (Create, Read, Update, Delete) to be useful.

In this step, you'll add the remaining endpoints so your API can handle the full lifecycle of a task. Each endpoint will return the correct HTTP status code and raise errors when a task doesn't exist.

In this step, get ready to:
  • Add GET endpoints to retrieve one or all tasks.
  • Add PUT and DELETE endpoints to update and remove tasks.
  • Test the full CRUD workflow in Swagger UI.
Add GET endpoints for reading tasks

Users need to retrieve tasks they've created. You'll build two GET endpoints: one that returns every task as a list, and one that looks up a single task by its ID.

  • In main.py, find the create_task function (the @app.post("/tasks") endpoint).
  • Copy the following code directly above it, between the root function and create_task:
@app.get("/tasks")
async def get_tasks() -> list[Task]:
    return tasks


@app.get("/tasks/{task_id}")
async def get_task(task_id: int) -> Task:
    for task in tasks:
        if task.id == task_id:
            return task
    raise HTTPException(status_code=404, detail="Task not found")

What does this code do?

These two endpoints handle the "Read" part of CRUD.

  • GET /tasks returns the entire tasks list. The -> list[Task] return type tells FastAPI what shape the response should be.
  • GET /tasks/{task_id} loops through stored tasks looking for a matching ID. If no match is found, it raises an HTTPException with a 404 status code.
  • Save main.py.
  • You won't see a visible change yet. In the final substep, you'll start the server and test all endpoints together.
Add PUT and DELETE endpoints

To complete the CRUD set, users need the ability to update existing tasks and remove ones they no longer need.

  • In main.py, scroll to the bottom of the file (after the create_task function).
  • Add the following two endpoints at the end of the file:
@app.put("/tasks/{task_id}")
async def update_task(task_id: int, task_data: TaskCreate) -> Task:
    for index, task in enumerate(tasks):
        if task.id == task_id:
            updated_task = Task(id=task_id, **task_data.model_dump())
            tasks[index] = updated_task
            return updated_task
    raise HTTPException(status_code=404, detail="Task not found")


@app.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int):
    for index, task in enumerate(tasks):
        if task.id == task_id:
            tasks.pop(index)
            return
    raise HTTPException(status_code=404, detail="Task not found")

What does this code do?

These two endpoints handle the Update and Delete parts of CRUD.

  • PUT /tasks/{task_id} finds the task by ID, then replaces its data with the incoming TaskCreate body while keeping the same ID. It returns the updated task.
  • DELETE /tasks/{task_id} removes the task from the list and returns status code 204 (No Content), meaning the deletion succeeded but there's nothing to send back.
  • Both raise a 404 if the task ID doesn't exist, just like get_task.
  • Save main.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException

from models import Task, TaskCreate

app = FastAPI()

tasks: list[Task] = []
task_id_counter: int = 1


@app.get("/")
async def root():
    return {"message": "Task Tracker API"}


@app.get("/tasks")
async def get_tasks() -> list[Task]:
    return tasks


@app.get("/tasks/{task_id}")
async def get_task(task_id: int) -> Task:
    for task in tasks:
        if task.id == task_id:
            return task
    raise HTTPException(status_code=404, detail="Task not found")


@app.post("/tasks", status_code=201)
async def create_task(task_data: TaskCreate) -> Task:
    global task_id_counter
    task = Task(id=task_id_counter, **task_data.model_dump())
    tasks.append(task)
    task_id_counter += 1
    return task


@app.put("/tasks/{task_id}")
async def update_task(task_id: int, task_data: TaskCreate) -> Task:
    for index, task in enumerate(tasks):
        if task.id == task_id:
            updated_task = Task(id=task_id, **task_data.model_dump())
            tasks[index] = updated_task
            return updated_task
    raise HTTPException(status_code=404, detail="Task not found")


@app.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int):
    for index, task in enumerate(tasks):
        if task.id == task_id:
            tasks.pop(index)
            return
    raise HTTPException(status_code=404, detail="Task not found")

Seeing a red underline or import error?

  • Make sure HTTPException is imported on the first line: from fastapi import FastAPI, HTTPException. This import was added in Step 2.
  • Check that TaskCreate is in your import from models: from models import Task, TaskCreate.

Still stuck? Help me fix an import error in my FastAPI main.py file

Test all endpoints manually using Swagger UI

Your API now has five endpoints covering the full CRUD lifecycle. Time to verify they all work correctly using FastAPI's auto-generated documentation.

  • Start the development server by running this command in your terminal:
fastapi dev main.py

What if the server is already running?

If you still have the server running from Step 2, it auto-reloads when you save files. You can skip this command and just refresh your browser.

  • Open http://127.0.0.1:8000/docs in your browser.
  • You should see all five endpoints listed: GET /, GET /tasks, GET /tasks/{task_id}, POST /tasks, PUT /tasks/{task_id}, and DELETE /tasks/{task_id}.

Now test the full workflow by trying each endpoint in order:

  • Click on POST /tasks to expand it, then click Try it out.
  • Replace the example JSON body with: {"title": "Buy groceries", "description": "Milk, eggs, bread"}.
  • Click Execute. You should see a 201 response with an auto-assigned id of 1.
  • Expand GET /tasks, click Try it out, then Execute. You should see a list containing the task you just created.
  • Expand GET /tasks/{task_id}, enter 1 for the task_id parameter, and click Execute. You should see your single task returned.
  • Try GET /tasks/{task_id} again with 999 as the ID. You should see a 404 response with the message Task not found.
  • Expand PUT /tasks/{task_id}, enter 1 for the ID, and set the body to: {"title": "Buy groceries", "completed": true}. Click Execute. You should see a 200 response with completed now set to true.
  • Expand DELETE /tasks/{task_id}, enter 1 for the ID, and click Execute. You should see a 204 response with no body.
  • Run GET /tasks one more time. The list should now be empty.

Endpoints not showing up in /docs?

  • Check your terminal for error messages. A syntax error (like a missing colon or incorrect indentation) will prevent the server from starting.
  • Make sure you saved main.py after adding both sets of endpoints.
  • Use the "double check the full code" tab above to compare your file line by line.

Help me debug why my FastAPI endpoints aren't showing up in the Swagger docs

  • Stop the dev server by pressing Ctrl+C in your terminal.
  • Commit your progress by running these commands:
git add .
git commit -m "Add full CRUD endpoints"

Your API now handles the complete task lifecycle. Next up, you'll write automated tests that verify every endpoint so you don't have to click through Swagger UI manually each time you make a change.

Write Automated Tests with pytest

Your API handles creating, reading, updating, and deleting tasks. You tested it manually through the Swagger UI, but manual testing is fragile.

Automated tests let you verify every endpoint in seconds, catch regressions instantly, and prove to employers that you write professional-grade code. In this step, you will build a test suite using pytest that covers all seven behaviors of your API.

In this step, get ready to:
  • Set up the test file with a test client and per-test reset logic.
  • Write test functions that verify every endpoint returns correct data and status codes.
  • Run the full test suite and commit your work.
Set up the test file

FastAPI includes a TestClient that simulates HTTP requests without starting a real server. You also need a way to reset the task list between tests so they don't affect each other.

  • In VS Code, open test_main.py and add the following imports and setup code:
from fastapi.testclient import TestClient

import main
from main import app

# Create a test client that sends requests to our app
client = TestClient(app)


# Runs automatically before each test function
def setup_function():
    main.tasks.clear()
    main.task_id_counter = 1

What does this code do?

TestClient wraps your FastAPI app so you can call client.get(), client.post(), etc. without running a real server.

The setup_function() is a special pytest hook that runs before every test function in the file. It clears the tasks list and resets the ID counter to 1, so each test starts with a clean slate.

  • Save test_main.py.

You won't see a change yet. The next chunk adds tests that use this setup.

Write tests for the root and create endpoints

Each test function name starts with test_ so pytest discovers it automatically. Each test makes a request and asserts the response matches what you expect.

  • In test_main.py, add the following two test functions below setup_function():
def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Task Tracker API"}


def test_create_task():
    response = client.post(
        "/tasks",
        json={"title": "Buy groceries", "description": "Milk, eggs, bread"},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Buy groceries"
    assert data["description"] == "Milk, eggs, bread"
    assert data["completed"] is False
    assert data["id"] == 1

What does this code do?

test_root() hits the root endpoint and checks it returns a 200 status with the welcome message JSON.

test_create_task() sends a POST request with task data and verifies the response has status 201, the correct fields, a default completed value of False, and an auto-assigned id of 1.

  • Save test_main.py.

You won't see a change yet. You will run all tests together after writing the remaining test functions.

Write tests for the read endpoints
  • Below test_create_task(), add these three test functions that verify reading tasks:
def test_get_tasks():
    # Create two tasks, then check the list returns both
    client.post("/tasks", json={"title": "Task 1"})
    client.post("/tasks", json={"title": "Task 2"})
    response = client.get("/tasks")
    assert response.status_code == 200
    assert len(response.json()) == 2


def test_get_task():
    # Create a task, then fetch it by ID
    client.post("/tasks", json={"title": "Test task"})
    response = client.get("/tasks/1")
    assert response.status_code == 200
    assert response.json()["title"] == "Test task"


def test_get_task_not_found():
    # Request a task ID that does not exist
    response = client.get("/tasks/999")
    assert response.status_code == 404

What does this code do?

test_get_tasks() creates two tasks and verifies the GET all endpoint returns a list of length 2.

test_get_task() creates a task and fetches it by ID, checking the title matches. test_get_task_not_found() verifies that requesting a non-existent ID returns a 404 status code.

  • Save test_main.py.

You won't see a change yet. The next chunk adds the final two tests.

Write tests for the update and delete endpoints
  • Below test_get_task_not_found(), add these two test functions:
def test_update_task():
    # Create a task, then update its title and status
    client.post("/tasks", json={"title": "Original"})
    response = client.put(
        "/tasks/1", json={"title": "Updated", "completed": True}
    )
    assert response.status_code == 200
    assert response.json()["title"] == "Updated"
    assert response.json()["completed"] is True


def test_delete_task():
    # Create a task, delete it, then verify the list is empty
    client.post("/tasks", json={"title": "To delete"})
    response = client.delete("/tasks/1")
    assert response.status_code == 204
    response = client.get("/tasks")
    assert len(response.json()) == 0

What does this code do?

test_update_task() creates a task, sends a PUT request with new data, and checks that the response reflects the updated title and completion status.

test_delete_task() creates a task, deletes it (expecting a 204 No Content response), then verifies the tasks list is now empty.

  • Save test_main.py.

Why does each test create its own data?

Because setup_function() clears the task list before every test, each test starts from zero. This means tests can run in any order without affecting each other. Isolated tests are easier to debug because a failure always points to exactly one behavior.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_main.py before moving on.

ⓧ I'd like to double check the full code

from fastapi.testclient import TestClient

import main
from main import app

client = TestClient(app)


def setup_function():
    main.tasks.clear()
    main.task_id_counter = 1


def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Task Tracker API"}


def test_create_task():
    response = client.post(
        "/tasks",
        json={"title": "Buy groceries", "description": "Milk, eggs, bread"},
    )
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Buy groceries"
    assert data["description"] == "Milk, eggs, bread"
    assert data["completed"] is False
    assert data["id"] == 1


def test_get_tasks():
    client.post("/tasks", json={"title": "Task 1"})
    client.post("/tasks", json={"title": "Task 2"})
    response = client.get("/tasks")
    assert response.status_code == 200
    assert len(response.json()) == 2


def test_get_task():
    client.post("/tasks", json={"title": "Test task"})
    response = client.get("/tasks/1")
    assert response.status_code == 200
    assert response.json()["title"] == "Test task"


def test_get_task_not_found():
    response = client.get("/tasks/999")
    assert response.status_code == 404


def test_update_task():
    client.post("/tasks", json={"title": "Original"})
    response = client.put(
        "/tasks/1", json={"title": "Updated", "completed": True}
    )
    assert response.status_code == 200
    assert response.json()["title"] == "Updated"
    assert response.json()["completed"] is True


def test_delete_task():
    client.post("/tasks", json={"title": "To delete"})
    response = client.delete("/tasks/1")
    assert response.status_code == 204
    response = client.get("/tasks")
    assert len(response.json()) == 0
Run the test suite
  • Run all your tests in verbose mode by running this command:
pytest -v

You should see output showing all 7 tests passing in green. Each line names the test function and shows PASSED next to it.

Seeing test failures?

  • Read the assertion error message. pytest shows you the expected value vs the actual value, which pinpoints the mismatch.
  • Make sure setup_function() references main.tasks and main.task_id_counter (not local copies). Without the main. prefix, your tests would modify their own variables instead of the app's data.
  • Check that you have import main as a separate line (not just from main import app). You need both so you can access main.tasks directly.

Why are my pytest tests failing?

  • Once all tests pass, commit your work by running these commands:
git add .
git commit -m "Add test suite for all API endpoints"

Why write tests for a portfolio project?

Employers look at test coverage as a signal of professional maturity. Writing tests shows you think about edge cases (like requesting a non-existent task) and that you value code reliability over just making something work once.

Your API is fully tested and version-controlled. Every endpoint has a test proving it works correctly. Ready for a challenge? The Secret Mission will push your skills further with priority levels and query parameter filtering.

Secret mission

Add Priority Levels and Filtering

Your API can create, read, update, and delete tasks. But what happens when a user has dozens of tasks and only wants to see the high-priority ones? In this mission, you'll add a priority system and query parameter filtering to make your API genuinely useful at scale.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop (contains main.py, models.py, test_main.py, requirements.txt, .gitignore, and the venv/ virtual environment).
  • pip's cached package files (stored globally on your system by pip).

Keep everything running

No action needed. Choose this if you're still actively building or want to revisit the project soon.

  • Your project stays in the folder you created on the Desktop.
  • To start the API server again, open your terminal, navigate to the project folder, activate the virtual environment with source venv/bin/activate, and run fastapi dev main.py.
  • To run your tests again, use pytest -v from the project folder with the virtual environment active.

Pause - I'll come back to this later

Shut down running processes to free up system resources. Your files remain on your Desktop for when you're ready to continue.

  • If the development server is still running in your terminal, press Ctrl+C to stop it.
  • Deactivate the virtual environment by running:
deactivate

Your project folder and all its files stay on your Desktop. When you come back, reactivate the virtual environment and pick up where you left off.

Delete - I don't want to use this again

Remove all project files and clean up cached packages to start fresh.

  • If the development server is still running, press Ctrl+C to stop it.
  • Deactivate the virtual environment if it is active:
deactivate
  • Delete the project folder from your Desktop. In your terminal, navigate to your Desktop and remove it:
cd ~/Desktop
rm -rf task-tracker-api

Folder name may differ

Replace task-tracker-api with whatever you named your project folder in Step 1.

  • Optionally, clear pip's cached package downloads by running:
pip cache purge

This removes downloaded package files that pip stores for future installs. It does not uninstall Python or VS Code from your machine.

Nice Work!

Nice Work!

Nice work! You just built a fully tested REST API from scratch using Python and FastAPI.

You've learned how to:

  • Built a REST API with 5 endpoints handling full CRUD operations using FastAPI.
  • Used Pydantic models to automatically validate incoming data on every request.
  • Wrote automated tests with pytest that verify every endpoint works correctly.
  • Secret Mission: Added enum-based priority levels and query parameter filtering to your API.

Ready to quiz yourself?