If you have ever built an API with Flask or Django REST Framework and found yourself writing the same validation, serialization, and documentation code by hand, FastAPI will feel like a cheat code. FastAPI is a modern, high-performance Python framework for building REST APIs, built on Starlette for the web layer and Pydantic for data validation — and it generates interactive API documentation for you automatically.
In this practical guide, you will build a complete FastAPI REST API from scratch: installation, CRUD endpoints, Pydantic validation, query parameters with pagination, proper error handling, response models, and dependency injection with Depends. By the end you will have a clean, working tasks API you can extend into a real project.
Verified and updated: 3 October 2026.
What you need before you start
You need Python 3.10 or newer and a terminal. The official FastAPI documentation now recommends uv for setup, but pip inside a virtual environment works exactly the same. Create a project folder and isolate your dependencies first — never install packages globally for a project.
mkdir fastapi-tasks-api
cd fastapi-tasks-api
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
Now install FastAPI with its standard extras. The [standard] extra pulls in the fastapi CLI (which gives you the fastapi dev command), Uvicorn, and other useful tooling in one shot:
pip install "fastapi[standard]"
That is the current recommendation straight from the official FastAPI installation guide — quoting the extra ("fastapi[standard]") keeps it working in every shell.
Your first FastAPI endpoint
Create a file called main.py and add this:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
Run the development server:
fastapi dev main.py
The fastapi dev command detects the FastAPI app in your file automatically and starts a Uvicorn server with auto-reload enabled. Open http://127.0.0.1:8000 and you will see your JSON response. Then visit http://127.0.0.1:8000/docs — FastAPI has already generated interactive Swagger UI documentation for your endpoint. No plugins, no config. An alternative ReDoc view lives at /redoc.
Note the decorators: @app.get("/") maps the GET HTTP method and the / path to your function. Returning a dict or list is enough — FastAPI serializes it to JSON for you.
Defining data models with Pydantic
Real APIs receive data, and you never want to trust it. FastAPI uses Pydantic models for request validation: declare the shape of your data with Python type hints, and FastAPI validates every incoming request against them automatically, returning a clear 422 error for anything invalid.
You are building a tasks API, so define what a task looks like:
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: str | None = None
priority: str = "medium"
class Task(TaskCreate):
id: int
done: bool = False
Splitting create and response models is a habit worth forming early: the client should never decide its own id or timestamps — the server owns those. The Field() constraints (min_length, max_length) are enforced on every request without you writing a single if statement.
Building CRUD endpoints in FastAPI
Wire the models up to route functions. For clarity, keep tasks in an in-memory dict for now — you will learn a cleaner project structure further down:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI()
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: str | None = None
priority: str = "medium"
class Task(TaskCreate):
id: int
done: bool = False
tasks: dict[int, Task] = {}
next_id = 1
@app.post("/tasks", response_model=Task, status_code=201)
def create_task(task: TaskCreate):
global next_id
new_task = Task(id=next_id, **task.model_dump())
tasks[next_id] = new_task
next_id += 1
return new_task
@app.get("/tasks", response_model=list[Task])
def list_tasks():
return list(tasks.values())
@app.get("/tasks/{task_id}", response_model=Task)
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}", response_model=Task)
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
return updated
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
if task_id not in tasks:
raise HTTPException(status_code=404, detail="Task not found")
del tasks[task_id]
Three details matter here. First, {task_id} in the path is a path parameter — the task_id: int type hint tells FastAPI to validate and convert it for you. Second, response_model=Task filters the response through the model, so stray internal fields never leak to clients and the docs stay accurate. Third, raise HTTPException(status_code=404, ...) is the idiomatic way to return errors; FastAPI converts it into a proper JSON error response.
Adding pagination with query parameters
Listing every record at once breaks the moment your dataset grows. FastAPI turns function parameters with defaults into query parameters automatically, so pagination is a few lines:
@app.get("/tasks", response_model=list[Task])
def list_tasks(skip: int = 0, limit: int = 20, done: bool | None = None):
result = list(tasks.values())
if done is not None:
result = [t for t in result if t.done == done]
return result[skip : skip + limit]
Now GET /tasks?skip=20&limit=10 pages through results and GET /tasks?done=true filters them. Because the parameters are typed, passing limit=abc returns a 422 validation error instead of crashing your code — and the query parameters show up in your auto-generated docs with their defaults.
Dependency injection with Depends
Depends is FastAPI’s superpower for real projects. Instead of repeating setup logic — database sessions, authentication, shared query parameters — you write it once as a function and inject it into any endpoint. FastAPI resolves dependencies before your route runs, caches each dependency’s result per request, and supports yield for setup-and-teardown (perfect for database sessions):
from fastapi import Depends
def get_db():
db = open_fake_session()
try:
yield db
finally:
db.close()
@app.get("/tasks")
def list_tasks(db=Depends(get_db)):
return db.query_tasks()
Everything before yield runs before the request; everything after runs after the response is sent, even if the endpoint raised an exception. The same pattern handles auth — a dependency that checks a token and raises a 401 stops unauthenticated requests before they ever reach your business logic. It also makes testing far easier: you override dependencies with test doubles via app.dependency_overrides instead of patching globals.
Structuring a FastAPI project that grows
A single main.py stops being fun around the fifth endpoint. For a project that survives contact with reality, split it up:
fastapi-tasks-api/
├── app/
│ ├── __init__.py
│ ├── main.py # app instance, middleware, router includes
│ ├── models.py # Pydantic schemas (TaskCreate, Task, ...)
│ ├── routers/
│ │ └── tasks.py # APIRouter with the /tasks endpoints
│ └── deps.py # shared dependencies (get_db, auth, ...)
└── tests/
└── test_tasks.py
Use APIRouter to keep endpoints in their own module, then mount it in main.py:
# app/routers/tasks.py
from fastapi import APIRouter
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.get("")
def list_tasks():
...
# app/main.py
from fastapi import FastAPI
from app.routers import tasks
app = FastAPI(title="Tasks API")
app.include_router(tasks.router)
The prefix keeps paths DRY, and tags groups endpoints into named sections in your Swagger docs automatically. API versioning follows the same pattern: APIRouter(prefix="/api/v1") for each version.
Testing your endpoints
FastAPI ships with TestClient (built on httpx), so you can test endpoints without running a server. Install pytest and httpx, then write tests that hit your real routes:
pip install pytest httpx
# tests/test_tasks.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_and_get_task():
response = client.post("/tasks", json={"title": "Write tests"})
assert response.status_code == 201
task_id = response.json()["id"]
response = client.get(f"/tasks/{task_id}")
assert response.status_code == 200
assert response.json()["title"] == "Write tests"
def test_missing_task_returns_404():
response = client.get("/tasks/9999")
assert response.status_code == 404
Run them with pytest. Because TestClient drives the actual ASGI app in-process, these tests exercise routing, validation, and error handling — not just your functions.
Common mistakes to avoid
- Blocking the event loop. If you use
async defendpoints, never call blocking libraries likerequestsortime.sleep()inside them — they freeze the entire event loop. Usehttpx.AsyncClientandasyncio.sleep()instead, or keep the endpoint as plaindefand let FastAPI run it in a thread pool. - Skipping
response_model. Without it, whatever your function returns goes straight to the client — including fields you meant to keep internal. - Letting clients set server-owned fields. Separate create schemas from response schemas so
id, timestamps, and computed fields stay under your control. - Deep URL nesting.
/users/3/projects/9/tasksgets painful fast. Two levels of nesting is the practical ceiling — beyond that, use a filter like/tasks?project_id=9.
Wrapping up
You now have a complete FastAPI REST API pattern: typed routes, Pydantic validation, paginated listings, honest error responses, dependency injection, a project layout that scales, and tests that prove it works. The interactive docs at /docs were generated from the code you already wrote — that is the FastAPI payoff: the type hints are the documentation.
Where to go next: add a real database with SQLAlchemy and Alembic, protect endpoints with OAuth2 or API keys via dependencies, and containerize the app with Docker and Uvicorn behind Nginx when you are ready to deploy.
Further Reading & References
- FastAPI official tutorial: First Steps — the official starting point; install, first app, and interactive docs.
- FastAPI official documentation — the authoritative reference for every feature covered here.
- Building a Modern API: Best Practices for 2026 and Beyond (dev.to) — resource-oriented routing and modern API design principles.
- FastAPI: Build a Python REST API in 5 Steps (2026) — a current end-to-end tutorial including JWT auth and testing with curl.
- FastAPI Boilerplate API user guide (GitHub) — endpoints, pagination, exception handling, and API versioning patterns in a production-style layout.
- FastAPI Tutorial #7: Dependency Injection with Depends() (YouTube) — Depends mechanics, per-request caching, and the database session pattern.
- FastAPI Tutorial #8: Class-Based & Sub-Dependencies (YouTube) — taking injection further with dependency chains and yield-based teardown.



