FastAPI has become the gold standard for Python web development in 2026, with version 0.136.1 released on April 23, 2026, powering production APIs at companies from Netflix to Uber. This tutorial walks you through building a complete production-ready REST API with FastAPI in 13 hands-on steps, covering Pydantic v2, async endpoints, JWT authentication, dependency injection, WebSockets, background tasks, automated testing, Docker deployment, and Kubernetes orchestration. By the end, you will have a working FastAPI project with full OpenAPI documentation, 90%+ test coverage, and a CI/CD-ready Docker image.
Whether you are migrating from Flask, Django, or Node.js, FastAPI’s automatic OpenAPI generation, type-hint-driven validation, and async-first architecture make it the fastest path to a modern Python API. This guide assumes basic Python knowledge, but every code block runs end to end on Python 3.12 or higher. Estimated time to complete the full project: 2 to 3 hours.
Why FastAPI Dominates Python Web Development in 2026
FastAPI’s rise from a 2019 side project to the most-recommended Python web framework in 2026 is a story of timing and engineering. Sebastian Ramirez built FastAPI on top of Starlette and Pydantic, two libraries that already solved hard problems: ASGI compatibility and runtime data validation. By stitching them together with Python’s type-hint system, FastAPI eliminated the boilerplate that made Flask and Django Rest Framework feel dated for greenfield API work. The framework now sits at the top of the Stack Overflow Python web framework satisfaction rankings and has been adopted by Netflix for internal tooling, Microsoft for Azure components, and Uber for ML model serving.
The performance story is a major differentiator. FastAPI runs on Uvicorn, an ASGI server built on uvloop and httptools, that consistently posts request-per-second numbers in the tens of thousands on commodity hardware. Compared to Flask 3.x synchronous request handling, FastAPI typically posts 2x to 4x higher throughput on I/O-bound workloads, and against Django Rest Framework the gap widens further on JSON-heavy endpoints because Pydantic v2’s Rust-backed core handles serialization in a fraction of the time of DRF serializers. For ML inference APIs, where every millisecond of latency translates to GPU cost, this is the deciding factor.
Beyond raw speed, the developer experience is what locks teams in. Type hints feed three systems at once: editor autocompletion, runtime validation, and OpenAPI 3.1 schema generation. The interactive Swagger UI and ReDoc pages render automatically at /docs and /redoc, so the API documentation never drifts from the implementation. Dependency injection through function parameters keeps database sessions, authentication, and configuration cleanly separated, and the framework’s first-party support for async/await means you can mix sync and async endpoints in the same app without wrapping anything in thread executors.
FastAPI vs Flask vs Django REST Framework in 2026
Choosing a Python web framework in 2026 has narrowed to three real options: FastAPI for new APIs, Django for full-stack apps with admin needs, and Flask for legacy or extremely simple services. The table below summarizes the practical differences that affect day-to-day work.
| Capability | FastAPI 0.136.1 | Flask 3.x | Django REST Framework 3.15 |
|---|---|---|---|
| Async support | Native async/await | Partial via async views | Limited (sync ORM core) |
| Auto OpenAPI docs | Built in (Swagger and ReDoc) | Requires Flask-Smorest or APISpec | Requires drf-spectacular |
| Runtime validation | Pydantic v2 (Rust-backed) | Marshmallow or manual | DRF serializers (Python) |
| Dependency injection | Built in via Depends() | None native | None native |
| WebSockets | First class via Starlette | Flask-Sock extension | Channels (separate stack) |
| Type-hint-driven API | Yes | No | No |
| Typical RPS (JSON echo) | 20k to 60k | 5k to 15k | 3k to 8k |
| Production-readiness | Used at Netflix, Uber, Microsoft | Mature ecosystem | Mature, opinionated |
The takeaway is straightforward: if you are building a new REST or GraphQL service that does not need the Django admin, FastAPI is the productivity winner. Flask is still the right choice for a tiny webhook or a one-file proof of concept, and Django remains unmatched when you need batteries-included auth, admin, and ORM in a monolith. Everywhere else, FastAPI’s combination of speed, type safety, and free documentation pages is hard to beat.
Prerequisites and Environment Setup
Before writing any code, lock down the toolchain. FastAPI 0.136.1 requires Python 3.10 or higher, but Python 3.12 is the recommended baseline in 2026 for performance, dependency compatibility, and access to recent typing features such as PEP 695 generics. Older runtimes like Python 3.8 and 3.9 will not install the latest FastAPI release. The other non-negotiables are a recent pip, a virtual environment tool, and Docker if you plan to follow the deployment steps. The full prerequisite checklist:
- Python: 3.12.x (3.10+ minimum)
- pip: 24.0 or higher (run
python -m pip install --upgrade pip) - FastAPI: 0.136.1 (latest as of April 23, 2026)
- Uvicorn: latest version with
standardextras - Pydantic: v2.x (installed automatically with FastAPI)
- SQLAlchemy: 2.x for the database layer
- pytest: 8.x and httpx for the test client
- Docker: latest stable for containerized deployment
- Editor: VS Code, PyCharm, or Cursor with the Python extension
- Operating system: Linux, macOS, or Windows with WSL2
Verify your Python install before proceeding. The output should report 3.10.x or higher; if it does not, install the current stable release from the official Python downloads page before continuing.
$ python --version
Python 3.12.4
$ python -m pip --version
pip 24.2 from /usr/local/lib/python3.12/site-packages/pip (python 3.12)
Step 1: Create the Project and Virtual Environment
Start with a clean directory and a per-project virtual environment so dependency versions do not collide with system Python or other projects. The venv module ships with the standard library, so no extra tooling is required. Activate the environment before running any pip command in the rest of this tutorial.
mkdir fastapi-tutorial && cd fastapi-tutorial
python -m venv .venv
# Linux or macOS
source .venv/bin/activate
# Windows PowerShell
.venvScriptsActivate.ps1
# Confirm activation
which python
# /home/you/fastapi-tutorial/.venv/bin/python
With the virtual environment active, create a minimal project layout. A flat structure works fine for a tutorial, but the convention this guide follows scales cleanly to multi-router production apps: an app/ package for source, a tests/ directory for pytest, and a top-level requirements.txt file. Create the directories with one command:
mkdir -p app/routers app/models tests
touch app/__init__.py app/main.py app/database.py app/dependencies.py
touch app/routers/__init__.py app/routers/items.py app/routers/users.py
touch tests/__init__.py tests/test_main.py
touch requirements.txt .env Dockerfile
Step 2: Install FastAPI and Pin Dependencies
FastAPI is still in the 0.x semver range, which means every minor release can introduce breaking changes. The official FastAPI versioning guide recommends pinning to a narrow range like >=0.136.1,<0.137.0 so an unattended pip install does not drag in a backwards-incompatible update overnight. Add the following to your requirements.txt:
# requirements.txt
fastapi>=0.136.1,<0.137.0
uvicorn[standard]>=0.30.0
pydantic>=2.7.0,<3.0.0
pydantic-settings>=2.3.0
sqlalchemy>=2.0.30
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
python-multipart>=0.0.9
httpx>=0.27.0
pytest>=8.2.0
pytest-asyncio>=0.23.0
Install everything in one go and freeze the resolved versions for reproducibility. The uvicorn[standard] extra pulls in uvloop and httptools, which is what unlocks the high-throughput numbers reported in the benchmarks earlier.
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip freeze > requirements.lock.txt
Step 3: Write Your First FastAPI Endpoint
Open app/main.py and add a tiny app to confirm that the install works. Three things to notice in the snippet below: the type hint on q: str | None = None drives both query parameter parsing and the OpenAPI schema, the path parameter item_id: int triggers automatic 422 errors when the caller passes a non-integer, and the async def declaration lets the function run cooperatively on the event loop without changing the call site.
# app/main.py
from fastapi import FastAPI
app = FastAPI(
title="FastAPI Tutorial 2026",
version="1.0.0",
description="Production-ready FastAPI starter built on Python 3.12.",
)
@app.get("/")
async def read_root():
return {"status": "ok", "framework": "FastAPI", "version": "0.136.1"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
Run the development server with hot reload. The --reload flag watches for file changes and restarts Uvicorn in milliseconds, which is the workflow you want during the rest of the tutorial.
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Expected output
INFO: Will watch for changes in these directories: ['/home/you/fastapi-tutorial']
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process using StatReload
INFO: Application startup complete.
Visit http://127.0.0.1:8000 in your browser to see the JSON response, then open http://127.0.0.1:8000/docs to see the auto-generated Swagger UI. Try changing item_id to a non-integer in the address bar; FastAPI returns a structured validation error without a single line of validation code on your part.
Step 4: Define Pydantic v2 Models for Request and Response
Pydantic v2’s Rust-backed core is one of the main reasons FastAPI 0.136.1 outperforms older Python frameworks. Instead of writing custom serializers, you declare data shapes as classes and FastAPI uses them for parsing, validation, OpenAPI schema generation, and response serialization. The Pydantic documentation details every available field type; the example below covers the patterns used in 80% of real APIs.
# app/models/schemas.py
from datetime import datetime
from pydantic import BaseModel, EmailStr, Field, ConfigDict
class ItemBase(BaseModel):
name: str = Field(min_length=1, max_length=120)
description: str | None = Field(default=None, max_length=2_000)
price: float = Field(gt=0, le=1_000_000)
in_stock: bool = True
class ItemCreate(ItemBase):
pass
class ItemRead(ItemBase):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=12, max_length=128)
full_name: str | None = None
class UserRead(BaseModel):
id: int
email: EmailStr
full_name: str | None = None
is_active: bool
model_config = ConfigDict(from_attributes=True)
Three Pydantic v2 details worth memorizing. First, ConfigDict(from_attributes=True) replaces the v1 orm_mode setting and is what lets FastAPI accept SQLAlchemy ORM instances as response payloads. Second, EmailStr requires the email-validator package, which Pydantic pulls in automatically on first use. Third, Field() constraints like min_length and gt become OpenAPI schema properties, so the Swagger UI shows the rules to API consumers.
Step 5: Add a SQLAlchemy 2.x Database Layer
Real APIs need persistence. SQLAlchemy 2.x is the de facto Python ORM, and its 2.0-style declarative mapping pairs cleanly with FastAPI’s dependency injection. For the tutorial, use SQLite to avoid running a separate database; the same code points at PostgreSQL or MySQL by changing the connection URL.
# app/database.py
from datetime import datetime
from sqlalchemy import create_engine, String, DateTime, Float, Boolean
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(
DATABASE_URL, connect_args={"check_same_thread": False}, echo=False
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(120), index=True)
description: Mapped[str | None] = mapped_column(String(2_000), nullable=True)
price: Mapped[float] = mapped_column(Float)
in_stock: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
hashed_password: Mapped[str] = mapped_column(String(255))
full_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
Base.metadata.create_all(bind=engine)
The Base.metadata.create_all() call creates the SQLite file and tables on first run. In production you replace this with Alembic migrations, but for a tutorial it keeps the moving parts to a minimum. Note the check_same_thread argument: it is required for SQLite plus FastAPI because the framework can route requests across threads, and the default SQLite driver objects to that.
Step 6: Use Dependency Injection for Database Sessions
FastAPI’s dependency injection system is the cleanest way to scope a database session to a single request. The Depends() helper takes any callable, runs it before the path operation, and passes the return value as a parameter. A generator function with a try/finally guarantees the session always closes even if the handler raises.
# app/dependencies.py
from typing import Annotated
from fastapi import Depends
from sqlalchemy.orm import Session
from app.database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
SessionDep = Annotated[Session, Depends(get_db)]
The Annotated[Session, Depends(get_db)] alias is the recommended FastAPI 2026 pattern. It saves you from writing db: Session = Depends(get_db) on every endpoint and keeps the type information clean for editors and OpenAPI generation. Use it consistently in every router that touches the database.
Step 7: Build a CRUD Router for Items
Routers split a FastAPI app into mountable modules so the main file does not balloon to thousands of lines. Each router lives in its own file, declares its own prefix and tags, and registers with the main app via app.include_router(). Below is a complete CRUD router for items, exercising path parameters, query parameters, request bodies, and response models.
# app/routers/items.py
from fastapi import APIRouter, HTTPException, status
from app.database import Item
from app.dependencies import SessionDep
from app.models.schemas import ItemCreate, ItemRead
router = APIRouter(prefix="/items", tags=["items"])
@router.post("/", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
async def create_item(payload: ItemCreate, db: SessionDep):
item = Item(**payload.model_dump())
db.add(item)
db.commit()
db.refresh(item)
return item
@router.get("/", response_model=list[ItemRead])
async def list_items(db: SessionDep, skip: int = 0, limit: int = 50):
return db.query(Item).offset(skip).limit(min(limit, 200)).all()
@router.get("/{item_id}", response_model=ItemRead)
async def read_item(item_id: int, db: SessionDep):
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return item
@router.put("/{item_id}", response_model=ItemRead)
async def update_item(item_id: int, payload: ItemCreate, db: SessionDep):
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
for key, value in payload.model_dump().items():
setattr(item, key, value)
db.commit()
db.refresh(item)
return item
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, db: SessionDep):
item = db.get(Item, item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
db.delete(item)
db.commit()
return None
Wire the router into the main app. Update app/main.py to import and include it; FastAPI handles prefix concatenation, tag grouping in Swagger, and OpenAPI schema merging automatically.
# app/main.py (additions)
from app.routers import items, users
app.include_router(items.router)
app.include_router(users.router)
Step 8: Add JWT Authentication with OAuth2 Password Flow
Real APIs need authentication. FastAPI ships first-class support for OAuth2 password bearer flow, which is the simplest secure pattern for username/password APIs. Combine it with a JWT and you get stateless authentication that survives load balancer rotation. The example below uses python-jose for the JWT and passlib with bcrypt for password hashing; both are pulled in by the requirements file in Step 2.
# app/security.py
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from passlib.context import CryptContext
from app.database import User
from app.dependencies import SessionDep
SECRET_KEY = "change-me-in-production-use-a-32-byte-random-string"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="users/token")
def hash_password(plain: str) -> str:
return pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def create_access_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {"sub": subject, "exp": expire}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: SessionDep,
) -> User:
credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
email: str | None = payload.get("sub")
if email is None:
raise credentials_exc
except JWTError:
raise credentials_exc
user = db.query(User).filter(User.email == email).first()
if user is None:
raise credentials_exc
return user
Add the user router with a token endpoint and a protected /me route. Notice how get_current_user is reused as a dependency on any path that needs authentication; FastAPI runs it before the handler and injects the resolved User object.
# app/routers/users.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.database import User
from app.dependencies import SessionDep
from app.models.schemas import UserCreate, UserRead
from app.security import (
hash_password,
verify_password,
create_access_token,
get_current_user,
)
router = APIRouter(prefix="/users", tags=["users"])
@router.post("/", response_model=UserRead, status_code=201)
async def register(payload: UserCreate, db: SessionDep):
if db.query(User).filter(User.email == payload.email).first():
raise HTTPException(status_code=409, detail="Email already registered")
user = User(
email=payload.email,
hashed_password=hash_password(payload.password),
full_name=payload.full_name,
)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/token")
async def login(
form: Annotated[OAuth2PasswordRequestForm, Depends()],
db: SessionDep,
):
user = db.query(User).filter(User.email == form.username).first()
if not user or not verify_password(form.password, user.hashed_password):
raise HTTPException(status_code=401, detail="Invalid credentials")
return {"access_token": create_access_token(user.email), "token_type": "bearer"}
@router.get("/me", response_model=UserRead)
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
return current_user
Step 9: Stream Real-Time Updates with WebSockets
FastAPI inherits Starlette’s WebSocket support, which means real-time bidirectional communication is a few lines of code. The handler below accepts a WebSocket connection, broadcasts every received message back to the client with a server timestamp, and cleanly closes on disconnect. The pattern scales to chat apps, live dashboards, and streaming model inference results from an LLM.
# app/routers/ws.py
from datetime import datetime, timezone
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
router = APIRouter()
@router.websocket("/ws/echo")
async def websocket_echo(websocket: WebSocket):
await websocket.accept()
try:
while True:
payload = await websocket.receive_text()
await websocket.send_json({
"echo": payload,
"server_time": datetime.now(timezone.utc).isoformat(),
})
except WebSocketDisconnect:
return
Test the endpoint quickly with the websockets CLI or a few lines of Python. Open two terminals, run the server in one, and send messages from the other to see the echo loop in action.
python -c "
import asyncio, websockets
async def main():
async with websockets.connect('ws://127.0.0.1:8000/ws/echo') as ws:
await ws.send('hello')
print(await ws.recv())
asyncio.run(main())
"
# {"echo": "hello", "server_time": "2026-04-26T19:21:08.901+00:00"}
Step 10: Offload Slow Work with BackgroundTasks
Some operations should not block the HTTP response: sending a confirmation email, writing an audit log, warming a cache. FastAPI’s BackgroundTasks object queues a callable to run after the response goes out, with the same event loop and dependency context as the request. For longer or distributed work, swap it for Celery or Arq, but BackgroundTasks covers the 80% case with zero infrastructure.
# app/routers/notifications.py
import logging
from fastapi import APIRouter, BackgroundTasks
from pydantic import BaseModel, EmailStr
logger = logging.getLogger("uvicorn.error")
router = APIRouter(prefix="/notifications", tags=["notifications"])
class EmailPayload(BaseModel):
to: EmailStr
subject: str
body: str
def send_email(payload: EmailPayload) -> None:
# Replace with SES, SendGrid, or aiosmtplib in production
logger.info("Sending email to %s: %s", payload.to, payload.subject)
@router.post("/email", status_code=202)
async def queue_email(payload: EmailPayload, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, payload)
return {"queued": True}
Step 11: Add Middleware, CORS, and Structured Logging
Production APIs need at least three cross-cutting concerns wired up: cross-origin resource sharing for browser clients, request-scoped logging for observability, and rate limiting for abuse protection. FastAPI inherits Starlette’s middleware system, so anything that conforms to the ASGI middleware contract drops in. The example below adds the official CORS middleware and a custom timing middleware that logs how long each request took.
# app/main.py (final shape)
import logging
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from app.routers import items, users, notifications, ws
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("api")
app = FastAPI(
title="FastAPI Tutorial 2026",
version="1.0.0",
description="Production-ready FastAPI starter built on Python 3.12.",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed_ms = (time.perf_counter() - start) * 1000
response.headers["X-Process-Time-ms"] = f"{elapsed_ms:.2f}"
logger.info("%s %s %s %.2fms",
request.method, request.url.path, response.status_code, elapsed_ms)
return response
app.include_router(items.router)
app.include_router(users.router)
app.include_router(notifications.router)
app.include_router(ws.router)
@app.get("/health", tags=["meta"])
async def health():
return {"status": "ok"}
Two production tips. Replace allow_origins=["*"] with an explicit allowlist in real deployments because wildcard CORS combined with cookies is a security mistake. For rate limiting, the slowapi package layers a Redis-backed limiter on top of FastAPI without writing custom middleware.
Step 12: Test Everything with pytest and TestClient
FastAPI’s TestClient wraps httpx and gives you a synchronous interface to the ASGI app, which makes it trivial to drive tests without spinning up a real server. Combine it with pytest fixtures and a per-test SQLite database to get an isolated, deterministic suite. Aim for at least 90% line coverage; FastAPI apps have so little glue code that this target is realistic.
# tests/test_main.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import Base, engine
client = TestClient(app)
@pytest.fixture(autouse=True)
def reset_db():
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
yield
def test_health():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_create_and_read_item():
create = client.post("/items/", json={
"name": "Mechanical keyboard",
"description": "65 percent layout",
"price": 129.99,
"in_stock": True,
})
assert create.status_code == 201
item_id = create.json()["id"]
read = client.get(f"/items/{item_id}")
assert read.status_code == 200
assert read.json()["name"] == "Mechanical keyboard"
def test_register_and_login():
register = client.post("/users/", json={
"email": "[email protected]",
"password": "correcthorsebatterystaple",
"full_name": "Ada Lovelace",
})
assert register.status_code == 201
login = client.post(
"/users/token",
data={"username": "[email protected]", "password": "correcthorsebatterystaple"},
)
assert login.status_code == 200
assert "access_token" in login.json()
def test_invalid_payload_returns_422():
response = client.post("/items/", json={"name": "", "price": -1})
assert response.status_code == 422
body = response.json()
assert "detail" in body
Run the suite and capture coverage in one command. The expected output below shows the four tests passing in well under a second on Python 3.12; that is what good FastAPI test ergonomics look like.
$ pytest -v --tb=short
============================= test session starts =============================
platform linux -- Python 3.12.4, pytest-8.2.0
collected 4 items
tests/test_main.py::test_health PASSED [ 25%]
tests/test_main.py::test_create_and_read_item PASSED [ 50%]
tests/test_main.py::test_register_and_login PASSED [ 75%]
tests/test_main.py::test_invalid_payload_returns_422 PASSED [100%]
============================== 4 passed in 0.61s ==============================
Step 13: Containerize with Docker and Deploy
The standard production deployment for FastAPI in 2026 is a slim Python image running Uvicorn with multiple workers behind a reverse proxy or a managed load balancer. The Dockerfile below uses a multi-stage build to keep the final image under 200 MB and runs the app as a non-root user. The official Docker getting started guide covers the basics if Docker is new to you.
# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
RUN useradd --create-home --shell /bin/bash app
WORKDIR /home/app
COPY --from=builder /install /usr/local
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Build the image, run it locally, and exercise the health endpoint. From there, push to Docker Hub or AWS ECR and deploy to ECS, Cloud Run, Fly.io, or any Kubernetes cluster. The image runs the same in every environment.
docker build -t fastapi-tutorial:1.0.0 .
docker run --rm -p 8000:8000 fastapi-tutorial:1.0.0
# In another terminal
curl -s http://localhost:8000/health
# {"status":"ok"}
For Kubernetes, a minimal deployment plus service manifest pairs FastAPI with horizontal pod autoscaling and rolling updates. The example below assumes you already have a cluster and a built image pushed to a registry.
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-tutorial
spec:
replicas: 3
selector:
matchLabels:
app: fastapi-tutorial
template:
metadata:
labels:
app: fastapi-tutorial
spec:
containers:
- name: api
image: your-registry/fastapi-tutorial:1.0.0
ports:
- containerPort: 8000
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: fastapi-tutorial
spec:
selector:
app: fastapi-tutorial
ports:
- port: 80
targetPort: 8000
type: ClusterIP
Common Pitfalls and How to Avoid Them
Even with FastAPI’s excellent defaults, a handful of mistakes show up in almost every codebase migrated from Flask or Django. Avoid them and you will spend less time debugging in production.
- Not pinning FastAPI versions: The 0.x semver range means a minor release can break your code. Pin to
>=0.136.1,<0.137.0inrequirements.txtand run integration tests before bumping. - Mixing sync and blocking I/O inside async endpoints: A blocking
requests.get()call in anasync defhandler stalls the event loop for every concurrent request. Usehttpx.AsyncClientorasyncio.to_thread()for blocking work. - Returning ORM models without
from_attributes=True: FastAPI cannot serialize SQLAlchemy instances unless the response model has the Pydantic v2ConfigDict(from_attributes=True)setting. - Storing secrets in source: Hard-coding the JWT
SECRET_KEYas in Step 8 is fine for a tutorial but disastrous in production. Load it from environment variables viapydantic-settings. - Disabling strict Content-Type checks: FastAPI 0.132.0 enabled strict JSON Content-Type by default. Some legacy clients send
text/plainfor JSON bodies; either fix the clients or override the parser explicitly. - Skipping the
--workersflag in production: A single Uvicorn worker uses one CPU core. Set--workersto roughly the number of cores, or run Uvicorn behind Gunicorn with the Uvicorn worker class. - Forgetting
connect_argswith SQLite: Without{"check_same_thread": False}, FastAPI threaded requests crash on SQLite immediately. - Wildcard CORS plus credentials: Combining
allow_origins=["*"]withallow_credentials=Trueis a security hole; browsers reject it, and modern lint tools flag it.
Advanced Tips for Production FastAPI
Once the basics are working, several advanced patterns separate hobby projects from production-grade FastAPI deployments. Use them selectively as your traffic and team size grow.
- Settings via pydantic-settings: Move every secret and feature flag into a
BaseSettingssubclass that reads from environment variables and a local.envfile. The result is type-checked configuration with zero runtime parsing code. - Async database drivers: SQLAlchemy 2.x supports asyncio engines through
asyncpgfor PostgreSQL oraiosqlitefor SQLite. Pair them withAsyncSessionin your dependency for true non-blocking database access. - Structured exception handlers: Register a global
app.exception_handler(SQLAlchemyError)handler that maps database errors to clean 500 responses with a stable error code your clients can branch on. - OpenAPI customization: Override
app.openapito inject security schemes, server URLs, and contact info. The result is documentation that doubles as API contract for client SDK generation. - Versioned routers: Mount routers under
/v1and/v2prefixes from day one. Cheap insurance against the day a breaking change becomes unavoidable. - Rate limiting with slowapi: Drop in a Redis-backed rate limiter that respects per-IP and per-user budgets without rewriting your endpoints.
- Observability with OpenTelemetry: The
opentelemetry-instrumentation-fastapipackage adds distributed tracing, metrics, and log correlation in three lines. - Hot reload off in production: Never run
--reloadin production; it disables several optimizations and adds disk-watch overhead.
Performance Benchmarks: FastAPI vs the Field
Benchmarks always come with caveats, but the directional gap between FastAPI and older Python frameworks is consistent across published 2025 to 2026 reports. The numbers below approximate community benchmarks (TechEmpower-style JSON serialization on a single 4-core box, identical hardware). Treat them as ballpark figures, not absolute promises, and re-run them on your own workload before making architectural calls.
| Framework | RPS (JSON serialization) | p50 latency | p99 latency | Memory per worker |
|---|---|---|---|---|
| FastAPI 0.136.1 + Uvicorn | 52,000 | 1.8 ms | 9 ms | 78 MB |
| Starlette 0.46 (raw) | 61,000 | 1.5 ms | 7 ms | 62 MB |
| Flask 3.0 + Gunicorn sync | 12,400 | 7.2 ms | 34 ms | 91 MB |
| Django 5.0 + Gunicorn sync | 6,900 | 13 ms | 58 ms | 140 MB |
| Node.js 22 + Express 4 | 48,000 | 2.0 ms | 10 ms | 72 MB |
| Go 1.22 + net/http | 110,000 | 0.9 ms | 4 ms | 22 MB |
Two things to take from the table. FastAPI is competitive with Node.js Express for I/O-bound JSON workloads, which is the surprising and often the decisive result for teams choosing between Python and Node. Go is still meaningfully faster, but for most product APIs the productivity gap to FastAPI dwarfs the throughput gap, and you can always pull a hot path into a Go microservice later.
Troubleshooting Common FastAPI Errors
The list below covers the eight most common errors developers hit during their first month with FastAPI, plus the fix that resolves each one in 90% of cases.
- 422 Unprocessable Entity on every request: Almost always a Pydantic validation failure. Check the
detailarray in the response body for the exact field and constraint that failed. - “Could not validate credentials” loop: The JWT
SECRET_KEYchanged between server restarts and existing tokens no longer verify. Set the key from an environment variable so it survives restarts. - SQLite “table is locked” errors: You forgot
connect_args={"check_same_thread": False}or you have multiple writer threads. Switch to PostgreSQL for concurrent writes. - CORS preflight failures: The browser sends
OPTIONSrequests that your CORS middleware does not allow. Add the missing methods or origins toCORSMiddleware. - “Pydantic v1 BaseModel is deprecated” warnings: A dependency is still importing from
pydantic.v1. Runpip list --outdatedand upgrade. - WebSocket connection closes immediately: Often a missing
await websocket.accept()at the top of the handler. - OpenAPI docs do not show new endpoint: The router was created but not included via
app.include_router(...), or you forgot to restart Uvicorn without--reload. - Background tasks never run: The handler returned before adding the task, or you raised an exception that aborted the response. Add the task before any code that can fail.
Frequently Asked Questions
What is the latest version of FastAPI in April 2026?
FastAPI 0.136.1 was released on April 23, 2026. It requires Python 3.10 or higher and is the recommended version to pin in new projects. Always pin a narrow range like >=0.136.1,<0.137.0 in requirements.txt because FastAPI is still in 0.x semver and minor releases can include breaking changes.
Is FastAPI faster than Flask and Django?
Yes, by a comfortable margin on I/O-bound JSON workloads. Community benchmarks consistently show FastAPI on Uvicorn delivering roughly 4x the requests per second of Flask 3.x with Gunicorn sync workers, and around 7x the throughput of Django 5.0 in the same conditions. The gap shrinks on CPU-bound work because all three frameworks are limited by Python’s GIL.
Do I need to learn async to use FastAPI?
No. FastAPI accepts both def and async def path operations, and the framework runs sync handlers in a thread pool automatically. Start with synchronous handlers if your team is new to asyncio, then migrate hot paths to async as you adopt async database drivers and HTTP clients like httpx.AsyncClient.
How does FastAPI handle authentication?
FastAPI provides built-in security utilities for OAuth2 password flow, OAuth2 with scopes, API key headers, HTTP Basic, and OpenID Connect. Step 8 in this tutorial implements the most common pattern, OAuth2 password flow with JWT bearer tokens. For OpenID Connect with providers like Auth0, Okta, or Keycloak, use fastapi-oauth2 or the underlying Authlib library.
Can FastAPI replace Django for full-stack apps?
For pure APIs and SPA backends, yes, and it usually does so with less code. For full-stack apps that lean heavily on Django’s admin, ORM, and templating system, the answer is no; you would end up rebuilding several Django subsystems and lose the productivity edge. A common 2026 pattern is to keep Django for the admin and CMS layer and add a FastAPI service for performance-sensitive APIs and ML model serving.
How do I deploy FastAPI to AWS, GCP, or Azure?
Containerize with the Dockerfile in Step 13 and deploy to AWS ECS or App Runner, Google Cloud Run, or Azure Container Apps; all three accept the same image. For Kubernetes, the manifests in Step 13 work on EKS, GKE, and AKS without modification. AWS Lambda is also viable through the mangum adapter, which translates API Gateway events to ASGI calls.
What is Pydantic v2 and why does FastAPI need it?
Pydantic v2 is a complete rewrite of Pydantic with a Rust-backed core (pydantic-core) that is roughly 5x to 50x faster at validation and serialization than the v1 pure-Python implementation. FastAPI 0.119.0 and later use Pydantic v2 by default, with a temporary pydantic.v1 shim for gradual migration. The performance improvement is one of the main reasons FastAPI 0.136.1 outperforms older Python web frameworks.
Where can I find the official FastAPI documentation?
The canonical reference lives at the official FastAPI documentation site, with release notes at the same domain and the source on the FastAPI GitHub repository. The Pydantic side is documented at the Pydantic documentation site, and the underlying ASGI server has its own pages on the Uvicorn project site.
Related Coverage
- How to Build a Flask REST API in 12 Steps [2026]
- Django vs Flask 2026: The Leading Python Framework Comparison
- Spring Boot Tutorial: Build a REST API in 13 Steps [2026]
- How to Master PostgreSQL 17: Complete Database Tutorial from Setup to Production [2026]
- How to Build a Task Queue with Celery Python and Redis in 13 Steps [2026]
- How to Get Started with Docker: Complete Beginner Tutorial [2026]
- How to Deploy Applications with Kubernetes and Helm: Complete Tutorial [2026]
Final Thoughts: When to Choose FastAPI in 2026
FastAPI 0.136.1 is the right choice in 2026 for any new Python REST or GraphQL API where developer productivity, runtime performance, and type-safe contracts matter. The combination of Pydantic v2’s Rust-backed validation, Starlette’s ASGI foundation, and Uvicorn’s high-throughput event loop produces a stack that comfortably handles tens of thousands of requests per second per worker on commodity hardware while keeping the code base smaller than the equivalent Flask or Django implementation. The framework’s automatic OpenAPI 3.1 schema generation eliminates the documentation drift that plagues older Python projects, and its dependency injection system makes shared concerns like database sessions, authentication, and configuration trivial to test.
Where FastAPI is not the right answer: a one-file webhook can stay on Flask, and a content-management-heavy app with admin, auth, and ORM tightly coupled belongs on Django. For everything else (microservices, ML inference APIs, internal tooling, mobile and SPA backends, real-time WebSocket services) FastAPI’s combination of speed, ergonomics, and ecosystem maturity makes it the dominant Python web framework heading into the second half of the decade. Pin your versions, write tests, deploy in containers, and the framework rewards disciplined teams with one of the best developer experiences in modern web development.
Sofia Lindström
Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.
View all articles