FastAPI Tutorial: Build REST APIs with Python (2026)
FastAPI has become the go-to framework for building high-performance REST APIs in Python. It combines async-first design, automatic OpenAPI documentation, and Pydantic-powered validation into a developer experience that outshines Flask for new API projects and challenges Django REST Framework for throughput-sensitive workloads. This guide walks you through everything — from your first endpoint to JWT authentication, async database access, and production deployment.
Table of Contents
- FastAPI vs Flask vs Django REST
- Installation and Project Setup
- Pydantic Models for Request and Response
- Path Parameters, Query Parameters, and Request Bodies
- Async Endpoints and Dependency Injection
- JWT Authentication with python-jose
- Async Database with SQLAlchemy 2.0
- Background Tasks
- Deployment with Uvicorn and Gunicorn
- Frequently Asked Questions
FastAPI vs Flask vs Django REST Framework
Choosing between FastAPI, Flask, and Django REST Framework (DRF) depends on your use case. Here is how they compare in 2026:
- FastAPI: Best for async, high-throughput APIs. Automatic docs, type safety via Pydantic, native async/await. No ORM bundled — you choose SQLAlchemy, Tortoise, or others.
- Flask: Minimalist, mature, sync-first. Great for simple APIs but requires many extensions (Flask-RESTful, Marshmallow, etc.). Async support via Quart or Flask 2.x async views is available but feels bolted on.
- Django REST Framework: Full-stack batteries-included. Best when you need Django's ORM, admin, and auth ecosystem. More opinionated and heavier than FastAPI for pure API services.
Benchmarks consistently show FastAPI matching or beating Node.js Express for throughput on async I/O-bound workloads. For new API-only projects, FastAPI is the clear choice in 2026.
Installation and Project Setup
# Install FastAPI with all optional dependencies
pip install "fastapi[standard]"
# Or install manually with uvicorn
pip install fastapi uvicorn[standard] pydantic
# For async SQLAlchemy and JWT support
pip install sqlalchemy[asyncio] asyncpg python-jose[cryptography] passlib[bcrypt]
Create a basic project structure:
myapi/
├── main.py
├── models.py
├── schemas.py
├── database.py
├── auth.py
└── routers/
├── users.py
└── items.py
Your minimal main.py:
from fastapi import FastAPI
app = FastAPI(
title="My API",
description="Production-ready FastAPI application",
version="1.0.0",
)
@app.get("/health")
async def health_check():
return {"status": "ok"}
# Run with: uvicorn main:app --reload
Pydantic Models for Request and Response
FastAPI uses Pydantic V2 for all data validation. Define schemas in schemas.py:
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class UserCreate(BaseModel):
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
created_at: datetime
is_active: bool = True
model_config = {"from_attributes": True} # replaces orm_mode in Pydantic V2
class ItemCreate(BaseModel):
title: str = Field(..., max_length=200)
description: Optional[str] = None
price: float = Field(..., gt=0, description="Price must be positive")
class ItemResponse(ItemCreate):
id: int
owner_id: int
Path Parameters, Query Parameters, and Request Bodies
from fastapi import FastAPI, HTTPException, Query, Path
from typing import Optional
app = FastAPI()
# Path parameter with validation
@app.get("/users/{user_id}")
async def get_user(
user_id: int = Path(..., gt=0, description="The user ID"),
):
# Raises 422 automatically if user_id is not a positive integer
return {"user_id": user_id}
# Query parameters with defaults and validation
@app.get("/items/")
async def list_items(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
search: Optional[str] = Query(None, min_length=2),
sort_by: str = Query("created_at", enum=["name", "price", "created_at"]),
):
return {
"skip": skip,
"limit": limit,
"search": search,
"sort_by": sort_by,
}
# Request body (POST)
@app.post("/items/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_db)):
db_item = Item(**item.model_dump())
db.add(db_item)
await db.commit()
await db.refresh(db_item)
return db_item
Async Endpoints and Dependency Injection
FastAPI's dependency injection system is one of its killer features. It handles teardown automatically and composes cleanly:
from fastapi import Depends, HTTPException, status
from typing import AsyncGenerator
# Database dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# Authentication dependency
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
credentials_exception = 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])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await db.get(User, username)
if user is None:
raise credentials_exception
return user
# Protected endpoint using dependencies
@app.get("/users/me", response_model=UserResponse)
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
async def whenever they perform I/O (database queries, HTTP calls, file operations). Use plain def for CPU-bound work — FastAPI automatically runs sync endpoints in a thread pool so they don't block the event loop.
JWT Authentication with python-jose
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
SECRET_KEY = "your-secret-key-store-in-env" # use os.getenv in production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode["exp"] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app.post("/token")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_db),
):
user = await authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token = create_access_token(
data={"sub": user.username},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
)
return {"access_token": access_token, "token_type": "bearer"}
Async Database with SQLAlchemy 2.0
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Boolean, DateTime, func
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/mydb"
engine = create_async_engine(DATABASE_URL, echo=True, pool_size=10, max_overflow=20)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
email: Mapped[str] = mapped_column(String(200), unique=True)
hashed_password: Mapped[str] = mapped_column(String(200))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
# Create tables on startup
@app.on_event("startup")
async def startup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# CRUD operations
from sqlalchemy import select
async def get_users(db: AsyncSession, skip: int = 0, limit: int = 100):
result = await db.execute(
select(User).where(User.is_active == True).offset(skip).limit(limit)
)
return result.scalars().all()
Background Tasks
FastAPI's BackgroundTasks runs work after the response is sent — perfect for sending emails, logging, or triggering webhooks without making the client wait:
from fastapi import BackgroundTasks
import smtplib
from email.message import EmailMessage
def send_welcome_email(email: str, username: str):
"""Runs in the background after the response is returned."""
msg = EmailMessage()
msg["Subject"] = "Welcome to MyAPI"
msg["From"] = "noreply@myapi.com"
msg["To"] = email
msg.set_content(f"Hi {username}, welcome aboard!")
with smtplib.SMTP("localhost") as smtp:
smtp.send_message(msg)
@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(
user: UserCreate,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
):
db_user = User(
username=user.username,
email=user.email,
hashed_password=hash_password(user.password),
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
# Schedule email — does NOT block the response
background_tasks.add_task(send_welcome_email, db_user.email, db_user.username)
return db_user
Deployment with Uvicorn and Gunicorn
For development, use uvicorn with reload. For production, use gunicorn as the process manager with uvicorn workers:
# Development
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Production — 4 workers, each with its own event loop
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 120 \
--access-logfile -
Docker example:
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000"]
2 × CPU cores + 1 workers. Since each uvicorn worker has its own async event loop, a 2-core machine handles 5 workers well.
Frequently Asked Questions
- Is FastAPI production-ready in 2026?
- Absolutely. FastAPI is used in production by companies like Microsoft, Uber, Netflix, and Explosion AI. It has a stable API, active development, and a large ecosystem.
- When should I use async def vs def in FastAPI?
- Use
async deffor endpoints that perform I/O (database queries, external HTTP calls). Usedeffor CPU-bound work — FastAPI runs sync endpoints in a thread pool automatically, so they won't block the event loop. - How does FastAPI compare to Flask for performance?
- FastAPI with async endpoints typically achieves 2-5x higher throughput than sync Flask on I/O-bound workloads. For purely CPU-bound work without I/O, the difference is minimal.
- Can FastAPI handle WebSockets?
- Yes. FastAPI has first-class WebSocket support via
@app.websocket("/ws"). You can combine REST and WebSocket endpoints in the same application. - Where is the automatic API documentation?
- FastAPI generates Swagger UI at
/docsand ReDoc at/redocautomatically from your code. No extra configuration needed. Disable in production if desired:FastAPI(docs_url=None, redoc_url=None).