I spent two years building REST APIs with Flask. Every time, I hit the same wall: the code worked fine, but documenting it was a nightmare. I’d write the endpoints, then spend another hour writing Swagger docs by hand, and by the time I was done, the docs were already out of sync with the code.

Then a colleague mentioned FastAPI. I was skeptical — another Python web framework? But after one afternoon with it, I understood why it’s blown up. FastAPI generates interactive API docs automatically, validates your request data with clear error messages, and runs on async performance that Flask can’t match without extensions. The docs thing alone saved me hours every sprint.

This tutorial walks you through building a real task management API with FastAPI. By the end, you’ll have a working CRUD API with automatic documentation, input validation, and proper HTTP status codes. Every command here has been tested.

What You’ll Build

A task manager REST API with these endpoints:

  • POST /tasks — Create a new task
  • GET /tasks — List all tasks (with optional filtering)
  • GET /tasks/{id} — Get a single task
  • PUT /tasks/{id} — Update a task
  • DELETE /tasks/{id} — Delete a task

Plus automatic Swagger UI docs at /docs and a machine-readable OpenAPI schema at /openapi.json.

Prerequisites

  • Python 3.8 or higher
  • A terminal and a text editor
  • Basic familiarity with HTTP methods (GET, POST, PUT, DELETE). If you’re newer to Python, my command-line todo app tutorial is a good warm-up.

Step 1: Install FastAPI and Uvicorn

FastAPI is the framework. Uvicorn is the ASGI server that runs it. Install both:

pip install fastapi uvicorn

You’ll see a lot of dependencies get pulled in — pydantic for data validation, starlette for the underlying HTTP toolkit, and anyio for async support. Don’t worry about those; FastAPI handles them for you.

Verify the installation:

python3 -c "import fastapi; print(f'FastAPI {fastapi.__version__} installed')"

You should see something like FastAPI 0.133.1 installed.

Step 2: Create Your First Endpoint

Create a file called main.py with this code:

from fastapi import FastAPI

app = FastAPI(title="Task Manager API", version="1.0.0")

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

That’s it. Six lines and you have a working API endpoint. The @app.get("/") decorator tells FastAPI: when someone sends a GET request to the root path, run this function and return the result as JSON.

Start the server:

uvicorn main:app --reload

The --reload flag watches your file for changes and restarts the server automatically — useful during development. You’ll see output like:

INFO:     Uvicorn running on http://127.0.0.1:8000
INFO:     Started reloader process

Open http://127.0.0.1:8000 in your browser, or test it from another terminal:

curl http://127.0.0.1:8000/

You’ll get back:

{"message":"Task Manager API is running"}

Step 3: Define Your Data Model with Pydantic

This is where FastAPI starts earning its keep. Instead of manually validating request data, you define a Pydantic model and FastAPI handles the rest — type checking, default values, and clear error messages when someone sends bad data.

Add these imports and models to the top of main.py:

from pydantic import BaseModel
from typing import Optional

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    completed: bool = False

class TaskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    completed: Optional[bool] = None

Two models here. Task is for creating new tasks — title is required, description and completed are optional. TaskUpdate is for partial updates where every field is optional (you only send what you want to change).

Here’s the practical difference: if someone sends a POST request without a title field, FastAPI automatically returns a 422 error with a clear message:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "title"],
      "msg": "Field required"
    }
  ]
}

No manual validation code. No if not request.json.get('title') scattered across your endpoints. FastAPI handles it based on the type hints in your model.

Step 4: Build the CRUD Endpoints

Now let’s add the core functionality. We’ll use an in-memory dictionary as our “database” (for a real project, you’d swap this for SQLAlchemy or a database driver).

Replace the contents of main.py with:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI(title="Task Manager API", version="1.0.0")

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    completed: bool = False

class TaskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    completed: Optional[bool] = None

tasks_db = {}
counter = 0

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

@app.post("/tasks", response_model=dict, status_code=201)
def create_task(task: Task):
    global counter
    counter += 1
    task_data = task.model_dump()
    task_data["id"] = counter
    tasks_db[counter] = task_data
    return task_data

@app.get("/tasks")
def list_tasks(completed: Optional[bool] = None):
    if completed is not None:
        return [t for t in tasks_db.values() if t["completed"] == completed]
    return list(tasks_db.values())

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

@app.put("/tasks/{task_id}")
def update_task(task_id: int, updates: TaskUpdate):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")
    task = tasks_db[task_id]
    update_data = updates.model_dump(exclude_unset=True)
    task.update(update_data)
    return task

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

Let’s break down what each endpoint does:

Creating a Task

The POST /tasks endpoint takes a Task model from the request body, assigns it an ID, stores it, and returns the full task data with a 201 Created status. The status_code=201 parameter in the decorator sets the correct HTTP status — not 200, which is the default for most frameworks.

Listing and Filtering

The GET /tasks endpoint returns all tasks, but here’s the useful part: you can filter by completion status. Send GET /tasks?completed=true and you get only finished tasks. The Optional[bool] = None parameter tells FastAPI this is an optional query parameter with a default of None (meaning “show everything”).

Updating with Partial Data

The PUT /tasks/{task_id} endpoint uses model_dump(exclude_unset=True) to only grab fields that were actually sent in the request. This means you can update just the title without touching the description, or flip completed without sending the other fields. Most APIs require you to send the full object for PUT requests — FastAPI makes partial updates painless.

Error Handling

The HTTPException calls return proper HTTP status codes. Hit a non-existent task ID with GET, and you get a 404 with a clear message. This is better than returning a 200 with an error message in the body — clients and proxies handle HTTP status codes correctly.

Step 5: Test Your API

With the server running (uvicorn main:app --reload), open a new terminal and test each endpoint (if you need a refresher on terminal commands, check my bash scripting guide):

Create a task:

curl -X POST http://127.0.0.1:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Write blog post", "description": "FastAPI tutorial"}'

Response (201 Created):

{"title":"Write blog post","description":"FastAPI tutorial","completed":false,"id":1}

List all tasks:

curl http://127.0.0.1:8000/tasks

Update a task:

curl -X PUT http://127.0.0.1:8000/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

Delete a task:

curl -X DELETE http://127.0.0.1:8000/tasks/1

Test the 404:

curl http://127.0.0.1:8000/tasks/999
{"detail":"Task not found"}

Step 6: Discover the Auto-Generated Docs

This is the part that convinced me to switch from Flask. While your server is running, open http://127.0.0.1:8000/docs in your browser. You’ll see Swagger UI — a full interactive documentation page that shows every endpoint, every parameter, and lets you test requests right from the browser.

No manual doc writing. No keeping docs in sync. Every time you add an endpoint, the docs update automatically.

The doc generation works because of those type hints in your Pydantic models. FastAPI reads them at startup and builds the schema. title: str becomes a required string field. Optional[str] = None becomes an optional string with a default. The framework uses your code as the source of truth.

Step 7: Add Query Parameters for Filtering

Let’s make the task list endpoint more useful by adding a search parameter. Update the list_tasks function:

from fastapi import Query

@app.get("/tasks")
def list_tasks(
    completed: Optional[bool] = None,
    search: Optional[str] = Query(None, description="Search tasks by title")
):
    results = list(tasks_db.values())
    if completed is not None:
        results = [t for t in results if t["completed"] == completed]
    if search:
        results = [t for t in results if search.lower() in t["title"].lower()]
    return results

Now you can query like this:

curl "http://127.0.0.1:8000/tasks?search=blog&completed=false"

The Query function gives you more control over query parameters — descriptions show up in the Swagger docs, and you can add validation constraints like min_length or regex patterns.

Where to Go from Here

This tutorial gives you the foundation. Here’s where I’d go next:

  • Add a real database — SQLAlchemy + SQLite or PostgreSQL for persistence. FastAPI has excellent SQLAlchemy integration.
  • Add authentication — FastAPI supports OAuth2 with JWT tokens out of the box. Check the official security tutorial.
  • Deploy with Docker — If you’ve read my guide on Docker Compose for local development, you already know how to containerize services. FastAPI apps containerize cleanly with a simple Dockerfile.
  • Add background tasks — FastAPI has built-in support for running functions after the response is sent, useful for sending emails or processing data.

If you’re coming from Flask, the switch is worth it. The auto-generated docs alone justify the migration. And if you’re starting a new Python API project from scratch, FastAPI should be your default choice — unless you have a specific reason to pick something else.

I’ve been building APIs for over a decade now, and FastAPI is the first framework where I genuinely enjoy writing the data models. The type hints do double duty: they validate your data AND document your API. That’s the kind of design that makes you wonder why every framework doesn’t work this way.

Filed under Tech & Gadgets
Last Update: June 6, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted