Build a Task Manager REST API

Build a REST API with FastAPI, Pydantic validation, and Swagger docs.

Introduction

30 Second Summary

Every app you use talks to a backend server to store and retrieve your data. That server exposes an API: a set of rules that lets the app ask for exactly what it needs.

In this project, you will build a Task Manager API from scratch using Python and FastAPI. You will create endpoints that let any client create, read, update, and delete tasks.

What You'll Build

A REST API that handles task management with full CRUD operations, data validation, error handling, and auto-generated interactive documentation.

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

  • A running FastAPI server with 6 endpoints for creating, reading, updating, and deleting tasks.
  • Pydantic data models that automatically validate incoming requests and reject bad data.
  • Proper error handling with HTTP status codes and query parameter filtering for flexible data retrieval.
  • Secret Mission: A statistics endpoint that returns task counts grouped by status and priority.

Are there any prerequisites?

You should be comfortable with basic Python concepts like functions, dictionaries, and type hints. No prior backend or API experience is needed.

Set Up Your Project

Every backend application starts with a well-organized workspace. Before you write any API code, you need a dedicated folder, an isolated Python environment, and the FastAPI framework installed.

In this step, you'll create your project workspace from scratch and install everything needed to start building your REST API.

In this step, get ready to:
  • Create a project folder and a Python virtual environment.
  • Install FastAPI with standard dependencies.
  • Create your main project file.
Create a project folder and virtual environment

A virtual environment isolates your project's dependencies from the rest of your system. This prevents version conflicts between projects.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open a terminal inside VS Code by clicking Terminal in the top menu bar, then New Terminal.

Your project requires Python 3.10 or higher because the code uses modern Python syntax.

  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.10 or higher

Your Python version is ready to go. Continue to the next instruction below.

ⓧ I see an older version

You need Python 3.10+ for this project. Download the latest version from the official Python website:

  • Go to python.org/downloads and download the latest release.
  • Run the installer. On Windows, check the Add python.exe to PATH box before clicking Install Now.
  • Close and reopen your terminal in VS Code, then re-run the version check command.

macOS users with an older system Python

macOS may ship with an older Python. After installing a newer version from python.org, you may need to use python3.10 (or whichever version you installed) explicitly in your terminal.

ⓧ Command not found

Python is not installed on your machine yet. Install it from the official website:

  • Go to python.org/downloads and download the latest release.
  • Run the installer. On Windows, check the Add python.exe to PATH box before clicking Install Now.
  • Close and reopen your terminal in VS Code, then run the version check command again.
  • Move to your Desktop by running this command:

macOS

cd ~/Desktop

Windows

cd ~\Desktop
  • Create your project folder and move into it by running these commands:
mkdir task-manager-api
cd task-manager-api
  • Create a Python virtual environment by running this command:

macOS

python3 -m venv venv

Windows

python -m venv venv

What is a virtual environment?

A virtual environment is a self-contained folder that holds its own copy of Python and any packages you install. This keeps your project's dependencies separate from other projects on your machine.

  • Activate your virtual environment by running this command:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate
  • Look at your terminal prompt. You should see (venv) at the beginning of the line.

Don't see (venv) in your prompt?

On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned and then try the activate command again.

On macOS, make sure you used source before the path. Without it, the command will not work.

help me activate my Python virtual environment

Install FastAPI

FastAPI is the framework you'll use to build your API. The standard install includes everything you need to run a development server.

  • Install FastAPI with its standard dependencies by running:
pip install "fastapi[standard]"

What does this command do?

pip install "fastapi[standard]" installs the FastAPI framework along with extras like uvicorn (the development server) and fastapi-cli (the command-line tool you'll use to start your API).

  • Wait for the installation to complete. You should see a message ending with Successfully installed followed by a list of packages.

Seeing an installation error?

Make sure your virtual environment is active. You should see (venv) in your terminal prompt. If not, re-run the activation command from the previous substep.

If you see a permissions error, do not use sudo. Activate the virtual environment first, then retry the install command.

help me install FastAPI in my virtual environment

Create your project file
  • Create your main project file by running this command:

macOS

touch main.py

Windows

New-Item main.py
  • Confirm the file exists by running:
ls
  • You should see main.py and the venv folder in the output.
  • Open main.py in the VS Code editor by clicking on it in the Explorer sidebar on the left.

Don't see the Explorer sidebar?

Press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle it open. If it says "No Folder Opened", click Open Folder and select your task-manager-api folder on the Desktop.

Your workspace is ready to go. Next up, you'll write your first API endpoint and see it respond in the browser.

Create Your First Endpoint

Your tools are installed and your project is ready. Now it's time to write code that brings your API to life.

An endpoint is a specific URL your server listens to and responds to when a request arrives. You'll write your first one, start the server, and confirm it's working in your browser.

In this step, get ready to:
  • Write a minimal FastAPI app with one endpoint.
  • Start the development server and see your first JSON response.
  • Explore the auto-generated API documentation.
Write a minimal FastAPI app

Every FastAPI app starts with three things: an import, an app instance, and at least one endpoint function. You'll add all three to your empty main.py file now.

  • Add the following code to your main.py file:
from fastapi import FastAPI


# Create the application instance
app = FastAPI()


# Define a GET endpoint at the root URL
@app.get("/")
def root():
    return {"message": "Task Manager API"}

What does this code do?

  • from fastapi import FastAPI brings in the FastAPI class from the library you installed.
  • app = FastAPI() creates your application object. This is the central piece that manages all your endpoints.
  • @app.get("/") is a decorator that tells FastAPI to call the function below it whenever someone makes a GET request to the root URL (/).
  • The root() function returns a Python dictionary. FastAPI automatically converts it to JSON for the response.
  • Save main.py.

Seeing a red underline on the import?

Make sure your virtual environment is activated in the terminal. VS Code may also need you to select the correct Python interpreter. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the one from your virtual environment.

Help me fix a red underline on my FastAPI import

✔️ 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


app = FastAPI()


@app.get("/")
def root():
    return {"message": "Task Manager API"}
Run the development server

Your code is saved. Now you need a server to actually listen for requests and send back responses. FastAPI includes a built-in development server that starts with a single command.

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

What does this command do?

fastapi dev starts a local development server using Uvicorn under the hood. It reads your main.py file, detects the FastAPI app object, and begins listening for requests at http://127.0.0.1:8000.

It also enables auto-reload. Whenever you save changes to your code, the server restarts automatically so you always see the latest version.

The terminal will keep running the server process. You should see output ending with a line that says Application startup complete.

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

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

Don't see the JSON response?

  • Check your terminal for error messages. A SyntaxError means something is wrong in main.py. Compare your code to the double-check tab above.
  • Make sure you saved main.py before running the command.
  • If you see Address already in use, another process is using port 8000. Press Ctrl+C to stop any previous server, then try again.

Help me fix my FastAPI dev server

Explore the automatic documentation

FastAPI auto-generates interactive API documentation from your code. You get this for free without writing a single extra line.

  • In your browser, navigate to http://127.0.0.1:8000/docs.

You should see the Swagger UI page listing your GET / endpoint.

  • Click on the GET / row to expand it.
  • Click Try it out.
  • Click Execute.

You should see the same {"message":"Task Manager API"} response appear in the **Response body** section below the button.

Why is auto-generated documentation useful?

In professional teams, frontend developers and API consumers use this documentation to understand what endpoints are available. Every endpoint you add from now on will automatically appear here with details about its parameters and expected responses.

You'll use this Swagger UI page throughout the rest of this project to test every endpoint you build.

Your first endpoint is live and documented. Next up, you'll define a data model so your API can accept and validate incoming task data.

Define Your Data Model with Pydantic

Your API is live and responding to requests. But returning a hardcoded welcome message is not very useful. A real task manager needs to handle structured data: tasks with titles, statuses, and priorities.

FastAPI uses Pydantic models to define the shape of incoming data. When a request arrives, Pydantic validates it automatically. If someone sends a task without a title, your API rejects it before your code even runs.

In this step, get ready to:
  • Create a Pydantic model that defines the structure of a task.
  • Set up in-memory storage for your tasks.
  • Build a POST endpoint that creates new tasks.
Create a TaskCreate model

A Pydantic model is a Python class that declares what fields your data must have and what types those fields are. FastAPI reads this model and uses it to parse and validate incoming JSON requests automatically.

  • In main.py, add a new import on line 2, directly below the existing from fastapi import FastAPI line:
from pydantic import BaseModel
  • Below the app = FastAPI() line, add the TaskCreate model by copying this code:
class TaskCreate(BaseModel):
    # Title is required - every task needs a name
    title: str
    # Optional field - defaults to None when not provided
    description: str | None = None
    # Defaults to "pending" for new tasks
    status: str = "pending"
    # Defaults to "medium" priority
    priority: str = "medium"

What does this code do?

This class inherits from BaseModel, which gives it automatic validation powers. Each field declaration tells Pydantic what to expect:

  • title: str means a title is required and must be a string. If a request omits it, FastAPI returns a 422 error automatically.
  • description: str | None = None means the description is optional. If omitted, it defaults to None.
  • status and priority have default values, so they are optional but always have a value.
  • Save main.py.
  • Check the terminal where your dev server is running. It should reload without errors.

Seeing an error in the terminal?

If the dev server shows an error after saving, check for a typo in from pydantic import BaseModel. Make sure pydantic is lowercase and the import is on its own line below the FastAPI import.

Also check that the class definition uses a colon after BaseModel and each field is indented with 4 spaces.

Help me fix a Pydantic import or class definition error in my FastAPI app.

Set up in-memory storage

Your API needs somewhere to store tasks between requests. For this project, you will use a simple Python dictionary. This keeps everything in memory and avoids the complexity of setting up a database.

  • In main.py, add these two variables below the TaskCreate class (leave a blank line between the class and the variables):
# In-memory storage for tasks
tasks: dict[int, dict] = {}
# Counter for generating unique task IDs
task_id_counter: int = 0

What does this code do?

tasks is a dictionary that maps integer IDs to task data. task_id_counter tracks the next ID to assign. Each time you create a task, the counter increments so every task gets a unique number.

This data lives only in memory. If you restart the server, all tasks disappear. A real production app would use a database, but this approach keeps you focused on the API logic.

You won't see a change in the browser yet. The next chunk uses these variables.

Create the POST endpoint

A POST request is how clients send new data to your API. You will create an endpoint that accepts a task in JSON format, assigns it an ID, stores it, and returns the new task to the client.

  • In main.py, add the POST endpoint below the existing root() function:
@app.post("/tasks", status_code=201)
def create_task(task: TaskCreate):
    # Access the global counter so we can increment it
    global task_id_counter
    task_id_counter += 1
    # Create a task dict with the new ID and all fields from the request
    new_task = {"id": task_id_counter, **task.model_dump()}
    # Store the task in our dictionary
    tasks[task_id_counter] = new_task
    return new_task

What does this code do?

@app.post("/tasks", status_code=201) registers this function to handle POST requests at /tasks. The status_code=201 tells FastAPI to return HTTP 201 (Created) instead of the default 200.

The parameter task: TaskCreate tells FastAPI to parse the incoming JSON body using your Pydantic model. If the JSON is invalid or missing required fields, FastAPI returns a detailed error automatically.

task.model_dump() converts the Pydantic model into a plain dictionary. The ** operator unpacks that dictionary so you can combine it with the new id field.

  • Save main.py.
  • Switch to your browser and navigate to http://127.0.0.1:8000/docs.
  • You should see a new POST /tasks endpoint listed in the Swagger UI.
  • Click on POST /tasks to expand it.
  • Click Try it out.
  • Replace the default JSON in the request body with this:
{
  "title": "Learn FastAPI",
  "description": "Build a task manager REST API",
  "status": "in-progress",
  "priority": "high"
}
  • Click Execute.
  • You should see a response with status code 201 and a JSON body containing your task with an id of 1.

Not seeing the POST endpoint in /docs?

Make sure you saved main.py after adding the new code. The dev server auto-reloads on save, so refresh the /docs page in your browser.

If the terminal shows an error, double-check that global task_id_counter is inside the function and that tasks and task_id_counter are defined above the function at the module level.

Help me debug why my POST /tasks endpoint is not showing up in Swagger UI.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and your server is running without errors.

ⓧ I'd like to double check the full code

Here is the complete main.py so far. Compare your file against this:

from fastapi import FastAPI
from pydantic import BaseModel


app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    description: str | None = None
    status: str = "pending"
    priority: str = "medium"


tasks: dict[int, dict] = {}
task_id_counter: int = 0


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


@app.post("/tasks", status_code=201)
def create_task(task: TaskCreate):
    global task_id_counter
    task_id_counter += 1
    new_task = {"id": task_id_counter, **task.model_dump()}
    tasks[task_id_counter] = new_task
    return new_task

Your API can now create tasks and validate incoming data automatically. Next up, you will add endpoints to read, update, and delete tasks to complete the full set of CRUD operations.

Build the Read, Update, and Delete Endpoints

Your API can create tasks, but there is no way to retrieve, modify, or remove them yet.

A complete REST API needs all four CRUD operations: Create, Read, Update, and Delete. In this step, you will add the remaining three so clients can fully manage tasks.

In this step, get ready to:
  • Build GET endpoints to list all tasks and retrieve a single task by ID.
  • Build a PUT endpoint to update an existing task.
  • Build a DELETE endpoint to remove a task from storage.
Add the read endpoints

Before adding the new endpoints, you need to prepare your import line for error handling you will add in the next step.

  • In main.py, find this import line at the top of your file:
from fastapi import FastAPI
  • Update it to also import HTTPException, so the line now looks like this:
from fastapi import FastAPI, HTTPException

Why add this import now?

You will use HTTPException in the next step to return proper error responses. Adding it now keeps your import line tidy as you build out the endpoints below.

Now you will add two GET endpoints. The first returns every task in your storage dictionary as a list. The second retrieves a single task by its ID.

  • Add the GET /tasks endpoint by copying this code below your existing create_task function:
@app.get("/tasks")
def get_tasks():
    # Return all stored tasks as a list
    return list(tasks.values())

What does this code do?

The @app.get("/tasks") decorator registers this function to handle GET requests at the /tasks path.

The tasks dictionary stores data as key-value pairs. Calling .values() extracts just the task data, and wrapping it in list() converts it to a JSON-friendly list.

  • Save main.py.
  • Switch to your browser and go to http://127.0.0.1:8000/docs.
  • First, use the POST /tasks endpoint to create a task (click Try it out, then Execute).
  • Then expand the GET /tasks endpoint, click Try it out, and click Execute. You should see a JSON list containing the task you just created.

Seeing an empty list?

Tasks are stored in memory. If your server restarted (because of a save with a syntax error), the storage resets. Create a new task with POST first, then try GET again.

Also make sure you saved main.py after adding the endpoint. The dev server only reloads when the file is saved.

Help me debug my GET /tasks endpoint

Now add the endpoint that retrieves a single task by its ID.

  • Add the GET /tasks/{task_id} endpoint directly below the get_tasks function you just wrote:
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    # Look up a single task by its integer ID
    return tasks[task_id]

What is a path parameter?

The {task_id} in the URL path is a path parameter. FastAPI extracts the value from the URL and passes it to your function.

By declaring task_id: int in the function signature, FastAPI automatically validates that the value is an integer. If someone passes a string like "abc", FastAPI returns a validation error automatically.

  • Save main.py.
  • In Swagger UI, expand the GET /tasks/{task_id} endpoint.
  • Click Try it out, enter 1 in the task_id field, and click Execute. You should see the specific task you created earlier.

Getting an Internal Server Error?

If you see a 500 error, your task storage may be empty (the server restarted). Use the POST endpoint to create a task first, then try GET with ID 1 again.

Help me fix my GET /tasks/{task_id} endpoint

Add the update endpoint

The PUT method replaces an existing resource with new data. Your update endpoint accepts both a path parameter (which task to update) and a request body (the new task data).

  • Add the PUT /tasks/{task_id} endpoint below the get_task function:
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: TaskCreate):
    # Replace the stored task with updated data
    updated_task = {"id": task_id, **task.model_dump()}
    tasks[task_id] = updated_task
    return updated_task

What does this code do?

This function takes two inputs: task_id from the URL path and task from the JSON request body. FastAPI knows which is which because task_id matches the path parameter name and task is a Pydantic model (so it comes from the body).

The **task.model_dump() syntax unpacks the validated task data into a dictionary. Combined with the "id" key, this creates a complete task record that replaces the old one in storage.

  • Save main.py.
  • In Swagger UI, expand the PUT /tasks/{task_id} endpoint.
  • Click Try it out, enter 1 for task_id, and change the status field to "in-progress" in the request body.
  • Click Execute. You should see the updated task returned with the new status value.

Not seeing the updated status?

Make sure you changed the status field inside the JSON body editor (not the task_id field above it). The body should look like: {"title": "string", "description": "string", "status": "in-progress", "priority": "medium"}.

Help me debug my PUT endpoint

Add the delete endpoint

The final CRUD operation removes a task from storage entirely. Your DELETE endpoint will pop the task out of the dictionary and return a confirmation message.

  • Add the DELETE /tasks/{task_id} endpoint below the update_task function:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    # Remove the task from storage and return it
    deleted_task = tasks.pop(task_id)
    return {"message": "Task deleted", "task": deleted_task}

What does this code do?

The .pop(task_id) method removes the task from the tasks dictionary and returns the removed value in one step.

The response includes both a confirmation message and the deleted task data. This lets the client confirm exactly which task was removed.

  • Save main.py.
  • In Swagger UI, expand the DELETE /tasks/{task_id} endpoint.
  • Click Try it out, enter 1 for task_id, and click Execute. You should see a JSON response with "message": "Task deleted" and the deleted task data.
  • Now try GET /tasks again. You should see an empty list, confirming the task was removed.

Getting a KeyError?

If you already deleted the task and try to delete it again, you will get an error because the ID no longer exists in storage. Create a new task with POST first, then try DELETE with the new ID.

Help me fix my DELETE endpoint error

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and all six endpoints appear in Swagger UI at http://127.0.0.1:8000/docs.

ⓧ I'd like to double check the full code

Here is the complete main.py file at this point:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    description: str | None = None
    status: str = "pending"
    priority: str = "medium"


tasks: dict[int, dict] = {}
task_id_counter: int = 0


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


@app.post("/tasks", status_code=201)
def create_task(task: TaskCreate):
    global task_id_counter
    task_id_counter += 1
    new_task = {"id": task_id_counter, **task.model_dump()}
    tasks[task_id_counter] = new_task
    return new_task


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


@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    return tasks[task_id]


@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: TaskCreate):
    updated_task = {"id": task_id, **task.model_dump()}
    tasks[task_id] = updated_task
    return updated_task


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    deleted_task = tasks.pop(task_id)
    return {"message": "Task deleted", "task": deleted_task}

Your API now supports full CRUD operations. Next up, you will add proper error handling so your API returns helpful error messages instead of crashing when something goes wrong.

Add Error Handling and Filtering

In the previous step, you built all six CRUD endpoints for your Task Manager API. Right now, if someone requests a task that does not exist, the API crashes with an ugly server error instead of returning a helpful message.

In this step, you will add proper error handling so your API returns clean 404 responses. You will also add filtering so clients can search tasks by status or priority.

In this step, get ready to:
  • Add 404 error handling to all task-ID endpoints.
  • Add query parameter filtering to the GET /tasks endpoint.
  • Test the complete API flow in Swagger UI.
Add HTTPException for missing tasks

When a client requests a task ID that does not exist, your API should return a 404 Not Found response instead of crashing. FastAPI provides HTTPException for exactly this purpose.

What is HTTPException?

HTTPException is a special error class that stops your endpoint and sends back an HTTP error response with a status code and message. When you raise it, FastAPI automatically returns a JSON response like {"detail": "Task not found"}.

Your main.py already imports HTTPException from the first line, so you are ready to use it.

  • In main.py, find the get_task function. It currently looks like this:
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    return tasks[task_id]
  • Replace the entire get_task function with the following version that checks whether the task exists first:
@app.get("/tasks/{task_id}")
def get_task(task_id: int):
    # Return a 404 error if the task ID does not exist
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    return tasks[task_id]

What does this code do?

if task_id not in tasks: checks whether the requested ID exists in your tasks dictionary. If it does not, raise HTTPException immediately stops the function and sends back a 404 response with a clear error message.

  • Now find the update_task function. It currently looks like this:
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: TaskCreate):
    updated_task = {"id": task_id, **task.model_dump()}
    tasks[task_id] = updated_task
    return updated_task
  • Replace it with the following version that includes the same 404 check:
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: TaskCreate):
    # Return a 404 error if the task ID does not exist
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    updated_task = {"id": task_id, **task.model_dump()}
    tasks[task_id] = updated_task
    return updated_task
  • Finally, find the delete_task function. It currently looks like this:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    deleted_task = tasks.pop(task_id)
    return {"message": "Task deleted", "task": deleted_task}
  • Replace it with this version:
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    # Return a 404 error if the task ID does not exist
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    deleted_task = tasks.pop(task_id)
    return {"message": "Task deleted", "task": deleted_task}
  • Save main.py.
  • Switch to your browser and open http://127.0.0.1:8000/docs.
  • Expand the GET /tasks/{task_id} section and click Try it out.
  • Enter 999 as the task_id and click Execute.

You should see a response with status code 404 and the body {"detail": "Task not found"}.

Still seeing a 500 Internal Server Error?

Make sure the if task_id not in tasks: check comes BEFORE the line that accesses tasks[task_id]. If the check is after the access, Python crashes before reaching it.

Also verify that raise is indented inside the if block. Python uses indentation to determine code structure.

Why is my endpoint still returning a 500 error instead of 404?

Add query parameter filtering

Right now, GET /tasks returns every single task. Clients often need to filter results. Query parameters let clients add filters to the URL like /tasks?status=pending.

How do query parameters work in FastAPI?

In FastAPI, any function parameter that is not part of the URL path is automatically treated as a query parameter. By giving the parameter a default value of None, you make it optional. Clients can include it or leave it out.

  • In main.py, find the get_tasks function. It currently looks like this:
@app.get("/tasks")
def get_tasks():
    return list(tasks.values())
  • Replace the entire get_tasks function with this version that accepts optional status and priority filters:
@app.get("/tasks")
def get_tasks(status: str | None = None, priority: str | None = None):
    # Start with all tasks
    result = list(tasks.values())
    # Filter by status if the client provided one
    if status:
        result = [t for t in result if t["status"] == status]
    # Filter by priority if the client provided one
    if priority:
        result = [t for t in result if t["priority"] == priority]
    return result

What does this code do?

status: str | None = None declares an optional query parameter. If a client visits /tasks?status=pending, FastAPI passes "pending" into the function. If they visit just /tasks, the value stays None and no filtering happens.

The list comprehension [t for t in result if t["status"] == status] keeps only tasks whose status matches what the client asked for.

  • Save main.py.
  • In Swagger UI at http://127.0.0.1:8000/docs, first create a task using POST /tasks with status set to pending.
  • Expand the GET /tasks section. You should now see status and priority fields in the parameters section.
  • Click Try it out, enter pending in the status field, and click Execute.

You should see only the tasks with "status": "pending" in the response.

Not seeing the status and priority fields?

Make sure you added status: str | None = None and priority: str | None = None as parameters inside the parentheses of def get_tasks(). FastAPI only recognizes them as query parameters if they are function arguments.

Why are my query parameters not showing up in Swagger UI?

Test the complete API flow

Your API now has error handling and filtering. Time to run through a full test to confirm everything works together.

  • In Swagger UI, create 3 tasks using POST /tasks with different statuses and priorities. For example:
{"title": "Write report", "status": "pending", "priority": "high"}
{"title": "Review code", "status": "in-progress", "priority": "medium"}
{"title": "Send email", "status": "completed", "priority": "low"}
  • Test filtering: use GET /tasks with status set to pending. You should see only the "Write report" task.
  • Test filtering by priority: use GET /tasks with priority set to high. You should see only the "Write report" task.
  • Test the 404 error: use GET /tasks/{task_id} with 999 as the ID. You should see the 404 response.
  • Test updating: use PUT /tasks/{task_id} to change one of your task's statuses to completed. Confirm the response shows the updated values.
  • Test deleting: use DELETE /tasks/{task_id} to remove one task. Then use GET /tasks to confirm it is gone.

What just happened?

You just tested the full lifecycle of a REST API: creating resources, reading them with filters, updating them, handling errors for missing resources, and deleting them. This create-read-update-delete pattern is the foundation of nearly every backend service.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.py and that all your tests pass in Swagger UI.

ⓧ I'd like to double check the full code

Here is the complete main.py at the end of this step:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel


app = FastAPI()


class TaskCreate(BaseModel):
    title: str
    description: str | None = None
    status: str = "pending"
    priority: str = "medium"


tasks: dict[int, dict] = {}
task_id_counter: int = 0


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


@app.post("/tasks", status_code=201)
def create_task(task: TaskCreate):
    global task_id_counter
    task_id_counter += 1
    new_task = {"id": task_id_counter, **task.model_dump()}
    tasks[task_id_counter] = new_task
    return new_task


@app.get("/tasks")
def get_tasks(status: str | None = None, priority: str | None = None):
    result = list(tasks.values())
    if status:
        result = [t for t in result if t["status"] == status]
    if priority:
        result = [t for t in result if t["priority"] == priority]
    return result


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


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


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    deleted_task = tasks.pop(task_id)
    return {"message": "Task deleted", "task": deleted_task}

Secret mission

Build a Task Statistics Endpoint

Your API can create, read, update, and delete tasks. But what if a dashboard wanted to show a summary of all tasks at a glance? Add a statistics endpoint that aggregates task counts by status and priority.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Your API server only runs while the terminal process is active.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • FastAPI dev server (running process in your terminal).
  • Project folder on your Desktop (contains main.py, virtual environment, and installed packages).

Keep everything running

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

  • Your dev server stays running at http://127.0.0.1:8000.
  • If you close the terminal, restart the server anytime by activating your virtual environment and running:
fastapi dev

Your task data resets each time the server restarts because it is stored in memory.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your project files stay on disk so you can pick up later.

  • In the terminal where the server is running, press Ctrl+C to stop the FastAPI dev server.
  • Deactivate your virtual environment by running:
deactivate

To resume later, open your project folder in VS Code, activate the virtual environment, and run fastapi dev again.

Delete - I don't want to use this again

Remove all project files and start fresh. This cannot be undone.

  • In the terminal where the server is running, press Ctrl+C to stop the FastAPI dev server.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf task-manager
  • On Windows, run:
cd ~\Desktop
rmdir /s /q task-manager

This removes your project folder, virtual environment, and all installed packages in one step.

Nice Work!

Nice Work!

You just built a fully functional REST API from scratch using FastAPI and Python. This is the exact kind of backend service that powers real-world web and mobile applications.

Here's what you learned:

  • Built a complete REST API with 6 endpoints covering all CRUD operations (GET, POST, PUT, DELETE) using FastAPI.
  • Used Pydantic models for automatic data validation and implemented error handling with proper HTTP status codes (201, 404).
  • Added query parameter filtering for flexible data retrieval and explored auto-generated Swagger UI documentation.
  • Secret Mission: Built a statistics endpoint that aggregates task data and learned about route ordering in FastAPI.

Ready to quiz yourself?