Build a Task Tracker CRUD API

Build a FastAPI task tracker with validated CRUD endpoints and interactive docs.

Introduction

30 Second Summary

A task rarely stays the same between being written down and being finished. A tracker needs to keep every change clear from the moment a task is added to the moment it is removed.

In this project, you will build a local Task Tracker API that makes the full CRUD request cycle visible. FastAPI's interactive documentation gives you a browser-based control panel for every request.

What You'll Build

Picture your browser showing a new task with its own numeric ID after you send your first request.

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

  • A working Task Tracker API that carries tasks through the full CRUD cycle from your browser.
  • Automatic request validation that catches invalid task data before your endpoint logic runs.
  • A clear 404 response when someone requests a task that does not exist.
  • Secret Mission: Add an optional completion-status filter to the task list.

Are there any prerequisites?

You'll need Python 3.10 or newer plus Visual Studio Code on Windows. Basic familiarity with Python functions, classes, dictionaries, and type hints is enough.

Before We Start

This is your moment to define what your Task Tracker API manages before the hands-on work begins. A clear goal keeps every CRUD operation connected to the full lifecycle of a task.

Set Up and Launch the Starter API

Your Task Tracker needs a reliable place to run before you add its CRUD operations. A small starter endpoint lets you verify the entire Windows toolchain with minimal code.

You will check Python first. You will then isolate the project's packages in a virtual environment before launching a local FastAPI server with interactive documentation.

In this step, get ready to:
  • Verify Python 3.10 or newer inside the task-tracker-api folder.
  • Create an isolated environment with the pinned project dependencies.
  • Launch the starter API and confirm its root endpoint appears in the interactive documentation.
Verify Python in your project folder

FastAPI 0.141.1 requires Python 3.10 or newer. Checking the runtime now prevents dependency failures later.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into Windows search.
  • Press Enter to open Visual Studio Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Click New Folder in the folder picker.
  • Name the folder task-tracker-api.
  • Select the task-tracker-api folder.

The folder picker can look different across Windows versions. Once the folder name appears at the top of the Visual Studio Code Explorer, you are in the right place.

  • Click Select Folder to load the folder into Visual Studio Code.
  • Click Terminal in the top menu bar.
  • Click New Terminal.
  • Click the terminal profile menu beside the plus icon.
  • Select Command Prompt to open a Windows Command Prompt in task-tracker-api.
  • Check your installed Python version by running this command:
python --version

You will see the installed Python version. Use the tab that matches your result.

✔️ I see version 3.10 or higher

Your Python runtime meets the project's requirement. You can create the isolated environment next.

ⓧ I see an older version

Your current runtime is below the project's minimum. The update path replaces that gap with a current Windows runtime.

  • Visit python.org/downloads in your browser.
  • Use the Python Install Manager from the download page to install a current runtime.
  • Return to the Windows Command Prompt terminal in Visual Studio Code after the installation finishes.
  • Repeat the version check by running the command below:
python --version

You should now see Python 3.10 or newer.

ⓧ Command not found

Windows cannot currently find a Python runtime. Installing one through the official manager makes the command available.

  • Visit python.org/downloads in your browser.
  • Use the Python Install Manager from the download page to install a current runtime.
  • Return to the Windows Command Prompt terminal in Visual Studio Code after the installation finishes.
  • Repeat the version check by running the command below:
python --version

You should now see Python 3.10 or newer.

Still seeing the old Python version?

Close the existing terminal with the trash icon. Create another Command Prompt terminal so Windows reloads its available commands.

If the old version remains, help me check which Python installation Windows is using.

Create the isolated environment

A virtual environment keeps this API's package versions separate from other Python projects. The requirements.txt file makes that environment reproducible.

  • Click the New File icon in the Visual Studio Code Explorer.
  • Name the file requirements.txt.
  • Pin the project's dependencies by pasting this code into requirements.txt:
fastapi==0.141.1
pydantic==2.13.5
uvicorn==0.52.4

What does this file do?

  • FastAPI provides the web framework and generates the interactive API documentation.
  • Pydantic validates the data your API receives in later steps.
  • Uvicorn runs the FastAPI application as a local web server.
  • Save requirements.txt.
  • Confirm that requirements.txt appears in the Visual Studio Code Explorer.
  • Create the local tutorial-env environment by running this command:
python -m venv tutorial-env

You will see the tutorial-env folder in the Explorer. That folder now holds an isolated Python runtime for this API.

  • Activate the environment by running this command:
tutorial-env\Scripts\activate

You will see the environment name at the start of the Command Prompt line. This confirms later package commands target the isolated environment.

  • Install the pinned dependencies by running this command:
python -m pip install -r requirements.txt

The installation can take a minute while Python downloads the packages. The final terminal lines show that the packages installed successfully.

Dependency installation failed?

Check that the prompt starts with the environment name. Confirm that requirements.txt contains all three pinned dependency lines.

For an environment-specific failure, help me troubleshoot this dependency installation.

Launch the starter API

A REST-style endpoint connects an HTTP method to a URL path. This starter uses a root GET endpoint so you can prove the application runs before adding task data.

  • Click the New File icon in the Visual Studio Code Explorer.
  • Name the file main.py.
  • Create the FastAPI application and its root endpoint by pasting this code into main.py:
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Task Tracker API is running"}

What does this code do?

  • FastAPI creates the application object that Uvicorn loads.
  • The root GET decorator connects requests for / to read_root().
  • The return annotation describes a response whose keys and values are strings.
  • The returned dictionary becomes the JSON response shown by the API.
  • Save main.py.
  • Predict whether the interactive documentation will recognize the root GET endpoint before you start the server.
  • Start the local development server by running this command:
uvicorn main:app --reload

Uvicorn keeps this terminal busy while the server runs. Its reload mode watches Python files so saved changes restart the application during development.

  • Open your browser.
  • Go to http://127.0.0.1:8000/docs.
  • Confirm that the interactive documentation lists a root GET endpoint.

You will see the root GET endpoint in the interactive documentation. This proves the isolated environment can import the pinned packages and serve main:app.

Documentation page not loading?

Keep the terminal running while the browser is open. Check that you launched the command from the task-tracker-api folder with the environment active.

If the terminal reports an import or startup problem, help me diagnose why my FastAPI documentation page is not loading.

✔️ Awesome, I've got everything!

Your starter API is live. Double-check that both project files are saved while the server continues running.

ⓧ I'd like to double check the full code

Compare each saved file with these complete versions.

fastapi==0.141.1
pydantic==2.13.5
uvicorn==0.52.4
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Task Tracker API is running"}

Your Windows toolchain now serves a documented FastAPI endpoint from an isolated environment. Next, you will turn that starter response into task creation and listing.

Create and List Tasks

Your starter FastAPI app is running with interactive documentation. The next slice turns JSON input into task objects you can retrieve.

A Pydantic model defines the shape of each task. A Python dictionary keeps those objects in memory so the CRUD mechanics stay visible without database setup.

In this step, get ready to:
  • Define the task data models and in-memory storage.
  • Create stored tasks through POST /tasks.
  • Return stored tasks through GET /tasks.
Define the task models and storage

The create model describes accepted input. Its type hints let FastAPI validate each request before your endpoint stores anything.

  • In main.py, select the opening lines from from fastapi import FastAPI through app = FastAPI().
  • Replace the selected lines with this block:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    completed: bool = False

How Does TaskCreate Work?

  • BaseModel lets Pydantic validate incoming task data against the model's type hints.
  • title requires text in every create request.
  • completed defaults to False when a request omits the completion status.
  • Save main.py.
  • Switch back to the Windows Command Prompt from earlier.

You'll see Uvicorn finish reloading without a traceback. This confirms the first model has valid Python syntax.

Does the Reload Show a Problem?

Check that BaseModel uses the same capitalization in the import and class definition. Confirm that both field lines are indented beneath TaskCreate.

Help me fix my TaskCreate model.

Create payloads require a title. Update payloads allow either field to be omitted so future partial updates can preserve existing values.

  • In main.py, place your cursor on the blank line below the TaskCreate class.
  • Add the update model with this block:
class TaskUpdate(BaseModel):
    title: str | None = None
    completed: bool | None = None

How Does TaskUpdate Work?

  • str | None allows the title to contain text or remain absent.
  • bool | None gives the completion status the same optional behavior.
  • None lets a future update distinguish an omitted field from a supplied value.
  • Save main.py.
  • Return to the Windows Command Prompt from earlier.

You'll see Uvicorn complete another reload without a traceback. The update model is now valid.

Does TaskUpdate Stop the Reload?

Confirm that each field contains a vertical bar between its type and None. Check that both field declarations are indented inside TaskUpdate.

Help me debug my TaskUpdate model.

The stored model extends the create model with a numeric ID. The dictionary maps each generated ID to its task object.

  • In main.py, place your cursor on the blank line below the TaskUpdate class.
  • Add the stored task model and storage values with this block:
class Task(TaskCreate):
    id: int


tasks: dict[int, Task] = {}
next_task_id = 1

How Does the Storage Work?

  • Task inherits title and completed from TaskCreate.
  • id identifies one stored task.
  • tasks starts as an empty dictionary keyed by integer IDs.
  • next_task_id starts at 1 for the first task.
  • Save main.py.
  • Return to the Windows Command Prompt from earlier.

You'll see Uvicorn reload successfully once more. Your models and in-memory storage are ready for an endpoint.

Does the Storage Code Fail to Load?

Confirm that Task appears after TaskCreate. Check that the dictionary annotation uses matching square brackets.

Help me debug my Task model and storage values.

Create tasks with POST

An HTTP POST request sends a new task to the API. The endpoint converts validated input into a stored Task with a generated ID.

  • In main.py, find the end of the read_root function.
  • Add the create endpoint below that function with this block:
@app.post("/tasks")
def create_task(task: TaskCreate) -> Task:
    global next_task_id
    new_task = Task(
        id=next_task_id,
        title=task.title,
        completed=task.completed,
    )
    tasks[next_task_id] = new_task
    next_task_id += 1
    return new_task

What Does the Create Endpoint Do?

  • @app.post("/tasks") connects POST requests at /tasks to create_task.
  • new_task combines the generated ID with the validated request fields.
  • tasks[next_task_id] stores the new object under its ID.
  • next_task_id += 1 prepares a different ID for the next request.
  • Save main.py.
  • Switch back to the interactive documentation from earlier.
  • Refresh the browser page.

You'll see POST /tasks in the documentation. The generated page now knows the request and response shapes.

  • Expand POST /tasks.
  • Click Try it out.
  • Replace the request body with {"title":"Learn CRUD","completed":false}.
  • Click Execute.

You'll see a response containing the title Learn CRUD. It also contains completed set to false plus the generated ID 1.

Don't See the Created Task?

Refresh the documentation if POST /tasks is missing. Confirm that the request body uses double quotes around both field names and the title.

Help me troubleshoot my POST task request.

List tasks with GET

The list endpoint reads every value from the shared dictionary. Its response changes as tasks are added to memory.

  • In main.py, find the end of the create_task function.
  • Add the list endpoint below that function with this block:
@app.get("/tasks")
def list_tasks() -> list[Task]:
    return list(tasks.values())

What Does the List Endpoint Do?

  • @app.get("/tasks") connects GET requests at /tasks to list_tasks.
  • tasks.values() reads every stored task object.
  • list[Task] describes the validated response as a list of task objects.
  • Save main.py.
  • Switch back to the interactive documentation from earlier.
  • Refresh the browser page.

Before you execute the first list request, predict whether the reloaded app contains an empty list or a populated list.

  • Expand GET /tasks.
  • Click Try it out.
  • Click Execute.

You'll see an empty list. Uvicorn reloaded the Python process after your edit, so the in-memory dictionary started fresh.

  • Expand POST /tasks.
  • Click Try it out.
  • Replace the request body with {"title":"Learn CRUD","completed":false}.
  • Click Execute.

You'll see the recreated task with ID 1. The object is now stored in the current process.

Before you execute the list request again, predict how the response changes after creating the task.

  • Return to the GET /tasks endpoint.
  • Click Execute again.

You'll see a list containing the Learn CRUD task with ID 1. Your API can now create validated tasks and return its stored collection.

Is the List Still Empty?

Run the POST request again after the latest Uvicorn reload. Confirm that you execute GET /tasks without saving main.py between the two requests.

Help me find why GET /tasks returns an empty list.

✔️ Awesome, I've got everything!

Your create and list endpoints are working. Make sure main.py is saved before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    completed: bool = False


class TaskUpdate(BaseModel):
    title: str | None = None
    completed: bool | None = None


class Task(TaskCreate):
    id: int


tasks: dict[int, Task] = {}
next_task_id = 1


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Task Tracker API is running"}


@app.post("/tasks")
def create_task(task: TaskCreate) -> Task:
    global next_task_id
    new_task = Task(
        id=next_task_id,
        title=task.title,
        completed=task.completed,
    )
    tasks[next_task_id] = new_task
    next_task_id += 1
    return new_task


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

What Does the Finished File Contain?

The full file combines your validated models with shared in-memory storage. Its two task endpoints create objects and return the current collection.

Your first CRUD slice is working. Next, you'll retrieve one task by ID and expose what happens when that ID does not exist.

Read a Task and Inspect the Failure

Your API can now create validated tasks. It can also return the full collection from in-memory storage.

Now you will retrieve one task through a path parameter. You will test the limits of a direct dictionary lookup with different IDs.

In this step, get ready to:
  • Add an endpoint that retrieves one task by ID.
  • Test the direct lookup with an existing ID.
  • Expose the failure caused by an unknown ID.
Add the task detail endpoint

The route /tasks/{task_id} captures an ID from the URL. The endpoint uses that ID as a key in tasks.

  • In main.py, place your cursor below the existing list_tasks function.
  • Extend main.py from that position by pasting this code:
@app.get("/tasks/{task_id}")
def read_task(task_id: int) -> Task:
    return tasks[task_id]

What does this endpoint do?

  • The @app.get("/tasks/{task_id}") decorator registers a detail route for task IDs.
  • The task_id: int annotation makes the path value a numeric ID.
  • The tasks[task_id] expression retrieves the matching Task object from the dictionary.
  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh the documentation page.

You will see a new GET /tasks/{task_id} operation. This confirms that the reloaded app includes read_task.

Don't see the detail endpoint?

Confirm that you saved main.py. Check that the new decorator begins at the left edge of the file.

Look at the Windows Command Prompt for a syntax error. Fix the indicated line before refreshing the documentation.

Help me troubleshoot why my FastAPI detail endpoint is missing from the interactive documentation.

✔️ Awesome, I've got everything!

Your task detail endpoint is saved. The interactive documentation now shows the new route.

ⓧ I'd like to double check the full code

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    completed: bool = False


class TaskUpdate(BaseModel):
    title: str | None = None
    completed: bool | None = None


class Task(TaskCreate):
    id: int


tasks: dict[int, Task] = {}
next_task_id = 1


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Task Tracker API is running"}


@app.post("/tasks")
def create_task(task: TaskCreate) -> Task:
    global next_task_id
    new_task = Task(
        id=next_task_id,
        title=task.title,
        completed=task.completed,
    )
    tasks[next_task_id] = new_task
    next_task_id += 1
    return new_task


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


@app.get("/tasks/{task_id}")
def read_task(task_id: int) -> Task:
    return tasks[task_id]
Retrieve an existing task

Saving main.py reloads the app process. Recreate a task so the new in-memory dictionary contains ID 1.

  • Expand POST /tasks in the interactive documentation.
  • Select Try it out.
  • Set title to Learn CRUD in the request body.
  • Set completed to false.
  • Select Execute.

The response contains task ID 1.

The task title is Learn CRUD. Its completion value is false.

  • Expand GET /tasks/{task_id}.
  • Select Try it out.
  • Enter 1 in the task_id field.

Before you execute the request, which stored task do you expect ID 1 to return?

  • Select Execute.

You will see the task with ID 1. The direct lookup works when the dictionary contains the requested key.

Inspect an unknown task ID

The known ID proves that the detail route can retrieve stored data. Testing an unknown ID reveals how the same lookup behaves outside its happy path.

  • Keep GET /tasks/{task_id} expanded.
  • Replace 1 with 999 in the task_id field.

Before you send the request, do you think the dictionary can return a task for an ID that was never stored?

  • Select Execute.
  • Switch back to the Windows Command Prompt running Uvicorn.

You will see that the request failed. The terminal traceback identifies a KeyError for the missing numeric key.

This Failure Is Intentional

A direct dictionary lookup assumes that the requested key exists. Task ID 999 has no matching entry, so tasks[task_id] raises a KeyError.

You have now exposed the unreliable missing-task behavior. This visible failure gives the next step a precise problem to solve.

You have proved that direct lookup works for stored IDs. Next, you will turn the missing-ID crash into predictable API behavior.

Complete CRUD Error Handling

Your FastAPI application can retrieve known task IDs. The direct dictionary lookup also exposed a KeyError when a task is missing.

A complete CRUD API needs predictable missing-task behavior across every operation. A shared lookup guard gives each operation the same response.

You will also complete partial updates. Deletion finishes the request cycle.

In this step, get ready to:
  • Convert missing task failures into clear 404 responses.
  • Add partial updates that preserve omitted task fields.
  • Delete tasks through the API.
Replace the crash with a reusable 404 guard

A lookup guard checks whether a task exists before returning it. Missing IDs can then produce a controlled response with a useful status code.

  • In main.py, find the current FastAPI import at the top of the file.
  • Use this reference to confirm the line:
from fastapi import FastAPI

What is missing from this import?

The current import provides the application class. The lookup guard also needs HTTPException to stop a request with a controlled error response.

  • Replace the current import with the expanded import below:
from fastapi import FastAPI, HTTPException

What does the expanded import provide?

The expanded import makes HTTPException available in main.py. Your guard can now return an intentional API error.

  • Replace the existing read_task() route with the shared guard and guarded route below:
def get_task_or_404(task_id: int) -> Task:
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]


@app.get("/tasks/{task_id}")
def read_task(task_id: int) -> Task:
    return get_task_or_404(task_id)

What does this guard do?

  • The get_task_or_404() function centralizes task lookup behavior.
  • The membership check catches an unknown ID before dictionary access can raise a KeyError.
  • The HTTPException response gives the request a 404 status with the detail Task not found.
  • The read_task() route now delegates every lookup to the guard.
  • Save main.py.
  • Return to the interactive documentation from earlier.
  • Refresh the browser.

The detail route is ready to test again after the server reload.

  • Expand GET /tasks/{task_id}.
  • Select Try it out.
  • Enter 999 in the task_id field.

Before you select Execute, do you expect the terminal to show another crash?

  • Select Execute.

You will see status 404 with {"detail":"Task not found"}. The terminal no longer shows a KeyError traceback for this request.

Still seeing the KeyError?

Confirm the import includes HTTPException. Check that read_task() returns the result from get_task_or_404(task_id).

Make sure you saved main.py before repeating the request.

Help me debug my missing-task guard.

Add partial task updates

A partial update changes only the fields included in a request. The existing TaskUpdate model makes both fields optional for this purpose.

  • Add the update route below read_task() in main.py by pasting this code:
@app.patch("/tasks/{task_id}")
def update_task(task_id: int, task: TaskUpdate) -> Task:
    stored_task = get_task_or_404(task_id)
    if task.title is not None:
        stored_task.title = task.title
    if task.completed is not None:
        stored_task.completed = task.completed
    return stored_task

How does the partial update work?

  • The route uses get_task_or_404() to retrieve the stored task safely.
  • Each is not None check identifies whether the request supplied that field.
  • Omitted fields keep their current values.
  • The return annotation documents the updated response as a Task.

Saving the file makes Uvicorn reload the application. The in-memory task collection starts empty after that reload.

  • Save main.py.
  • Return to the interactive documentation.
  • Refresh the browser.

You will see the new PATCH /tasks/{task_id} operation in the documentation.

  • Expand POST /tasks.
  • Select Try it out.
  • Replace the example title in the request body with Learn CRUD.
  • Set completed to false.
  • Select Execute.

You will receive a new task with ID 1.

  • Expand PATCH /tasks/{task_id}.
  • Select Try it out.
  • Enter 1 in the task_id field.
  • Edit the request body to include only completed set to true.
  • Select Execute.

The response keeps the title Learn CRUD. Its completed value is now true.

Did the title disappear?

Check that each assignment sits inside its matching is not None condition. Confirm that the function returns stored_task after both checks.

Help me debug my partial task update.

Delete tasks and verify the full cycle

Deletion removes a task from the in-memory dictionary. The same lookup guard ensures an unknown task receives the established missing-task response.

  • Add the delete route below update_task() in main.py by pasting this code:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int) -> dict[str, str]:
    deleted_task = get_task_or_404(task_id)
    del tasks[task_id]
    return {"message": f"Deleted task {deleted_task.id}"}

What does the delete route do?

  • The guard retrieves the task before deletion.
  • The del statement removes the matching dictionary entry.
  • The response confirms which numeric task ID was deleted.
  • Save main.py.
  • Return to the interactive documentation.
  • Refresh the browser.

You will see the DELETE /tasks/{task_id} operation. The reload also resets the in-memory task collection.

  • Expand POST /tasks.
  • Select Try it out.
  • Replace the example title with Learn CRUD.
  • Set completed to false.
  • Select Execute.

You will receive task ID 1 from the create operation.

  • Expand GET /tasks/{task_id}.
  • Select Try it out.
  • Enter 1 in the task_id field.
  • Select Execute.

The response shows the task with the title Learn CRUD.

  • Expand PATCH /tasks/{task_id}.
  • Select Try it out.
  • Enter 1 in the task_id field.
  • Edit the request body to include only completed set to true.
  • Select Execute.

The updated response keeps the original title. Its completion value changes to true.

  • Expand DELETE /tasks/{task_id}.
  • Select Try it out.
  • Enter 1 in the task_id field.

Before you select Execute, what response do you expect after deleting task ID 1?

  • Select Execute.

You will see {"message":"Deleted task 1"} in the response.

  • Return to GET /tasks/{task_id}.
  • Enter 1 in the task_id field.

Before you execute the final read, do you expect the deleted task to remain available?

  • Select Execute.

You will see status 404 with {"detail":"Task not found"}. That completes the full create, read, update, delete, and missing-task cycle.

Can you still retrieve the deleted task?

Confirm the delete route runs del tasks[task_id] before returning its message. Check that your final read uses the same ID you deleted.

Help me debug why my deleted task remains available.

Use the tabs below to compare your completed main.py with the target file.

✔️ Awesome, I've got everything!

Your saved main.py now matches the completed CRUD API.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    completed: bool = False


class TaskUpdate(BaseModel):
    title: str | None = None
    completed: bool | None = None


class Task(TaskCreate):
    id: int


tasks: dict[int, Task] = {}
next_task_id = 1


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Task Tracker API is running"}


@app.post("/tasks")
def create_task(task: TaskCreate) -> Task:
    global next_task_id
    new_task = Task(
        id=next_task_id,
        title=task.title,
        completed=task.completed,
    )
    tasks[next_task_id] = new_task
    next_task_id += 1
    return new_task


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


def get_task_or_404(task_id: int) -> Task:
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]


@app.get("/tasks/{task_id}")
def read_task(task_id: int) -> Task:
    return get_task_or_404(task_id)


@app.patch("/tasks/{task_id}")
def update_task(task_id: int, task: TaskUpdate) -> Task:
    stored_task = get_task_or_404(task_id)
    if task.title is not None:
        stored_task.title = task.title
    if task.completed is not None:
        stored_task.completed = task.completed
    return stored_task


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int) -> dict[str, str]:
    deleted_task = get_task_or_404(task_id)
    del tasks[task_id]
    return {"message": f"Deleted task {deleted_task.id}"}

Secret mission

Filter Tasks by Completion Status

Your Task Tracker API currently returns every stored task in one list. In this mission, you will add an optional completion-status filter while preserving the original response when no filter is supplied.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local resources running, pause your work for another session, or remove the project. Everything runs on your computer, so these resources do not create ongoing charges.

Resources you used:

  • The running local Uvicorn development server.
  • The local task-tracker-api folder containing main.py plus requirements.txt.
  • The disposable tutorial-env virtual environment inside task-tracker-api.

Keep everything running

No action needed. Choose this if you are still testing your task API or planning to build on it soon.

  • Leave the Uvicorn server running while you continue testing the endpoints.
  • Keep the task-tracker-api folder unchanged for future development.
  • Keep tutorial-env available so the pinned packages remain isolated from your other Python projects.

Pause - I'll come back to this later

Shut down the running process to free your terminal. Your project files stay intact for your next session.

  • Press Ctrl+C in the Windows Command Prompt from earlier to stop Uvicorn.
  • Run deactivate in the same terminal to leave the virtual environment.
  • Close Visual Studio Code.
  • Reopen task-tracker-api in Visual Studio Code when you return.
  • Run tutorial-env\Scripts\activate before restarting the server.
  • Restart Uvicorn with the development command from Step 1.

Delete - I don't want to use this again

Remove all project resources when you do not plan to return. This clears the code plus the virtual environment from the folder's current location.

  • Press Ctrl+C in the Windows Command Prompt from earlier to stop Uvicorn.
  • Close Visual Studio Code.
  • Press the Windows key to open search.
  • Type File Explorer into the search bar.
  • Press Enter to open Windows File Explorer.
  • Locate the task-tracker-api folder you used for this project.
  • Right-click the task-tracker-api folder.
  • Select Delete.

The task-tracker-api folder disappears from its original location. This removes main.py, requirements.txt, and tutorial-env together.

Nice Work!

Nice Work!

You did it! Your Task Tracker API now supports the full CRUD cycle through FastAPI's interactive documentation.

You've learned how to:

  • Create validated task objects with Pydantic models. Store them in an in-memory collection.
  • Implement REST-style endpoints for every CRUD operation. Match each operation to its HTTP method.
  • Add reusable missing-task handling through get_task_or_404. Return 404 with Task not found for unknown IDs.
  • Secret Mission: Extend list_tasks with optional completion-status filtering. Preserve the full task list when no filter is supplied.

Ready to quiz yourself?