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 taskGET /tasks— List all tasks (with optional filtering)GET /tasks/{id}— Get a single taskPUT /tasks/{id}— Update a taskDELETE /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.