FastAPI has become my go-to framework for building Python APIs. It combines the simplicity of Flask with automatic request validation, OpenAPI documentation generation, and first-class async support — all powered by Python's type hints and Pydantic for data validation.
With Pydantic v2 (a complete rewrite in Rust), validation performance improved by 5-50x depending on the workload. If you're building APIs in Python, this combination is hard to beat.
Why FastAPI?
FastAPI's core idea is that your Python type annotations should be the single source of truth. Define your types once, and FastAPI uses them for request validation, response serialization, API documentation, and editor autocompletion. There's no separate schema file, no validation middleware, and no documentation that drifts out of sync.
main.py — A complete API in 30 lines
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from datetime import datetime
app = FastAPI(title="User API", version="1.0.0")
class UserCreate(BaseModel):
name: str
email: EmailStr
age: int | None = None
class UserResponse(BaseModel):
id: int
name: str
email: str
created_at: datetime
users_db: dict[int, UserResponse] = {}
next_id = 1
@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
global next_id
db_user = UserResponse(
id=next_id, name=user.name, email=user.email,
created_at=datetime.now()
)
users_db[next_id] = db_user
next_id += 1
return db_user
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
return users_db[user_id]
Run this with uvicorn main:app --reload and you immediately get a fully documented API at /docs (Swagger UI) and /redoc (ReDoc), with request validation that returns clear error messages for invalid inputs.
Pydantic v2: The Validation Engine
Pydantic v2 is a complete rewrite with a Rust core (pydantic-core). The performance improvements are dramatic, but the API changes also make it more capable:
Advanced Pydantic v2 models
from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Literal
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
price: float = Field(gt=0, description="Price in USD")
category: Literal["electronics", "books", "clothing"]
tags: list[str] = Field(default_factory=list, max_length=10)
@field_validator("name")
@classmethod
def name_must_be_titlecase(cls, v: str) -> str:
return v.strip().title()
@model_validator(mode="after")
def validate_price_for_category(self):
if self.category == "books" and self.price > 500:
raise ValueError("Book price seems unreasonably high")
return self
Dependency Injection
FastAPI's dependency injection system is one of its strongest features. It lets you declare what each endpoint needs, and FastAPI resolves and provides those dependencies automatically:
Dependencies for database sessions and auth
from fastapi import Depends, Header
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
async def get_current_user(
authorization: str = Header(...),
db: AsyncSession = Depends(get_db),
) -> User:
token = authorization.removeprefix("Bearer ")
user = await verify_token(token, db)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
@app.get("/me", response_model=UserResponse)
async def get_profile(user: User = Depends(get_current_user)):
return user
Dependencies can depend on other dependencies, forming a graph that FastAPI resolves automatically. This keeps your endpoint functions clean — they declare what they need, and the framework handles the wiring.
Async All the Way
FastAPI is built on Starlette and supports async natively. For I/O-bound workloads (database queries, HTTP calls, file operations), async endpoints handle significantly more concurrent requests than synchronous equivalents:
Async endpoint with concurrent operations
import asyncio
from httpx import AsyncClient
@app.get("/dashboard")
async def get_dashboard(user: User = Depends(get_current_user)):
async with AsyncClient() as client:
# Run multiple API calls concurrently
profile_task = client.get(f"/api/profile/{user.id}")
stats_task = client.get(f"/api/stats/{user.id}")
notifications_task = client.get(f"/api/notifications/{user.id}")
profile, stats, notifications = await asyncio.gather(
profile_task, stats_task, notifications_task
)
return {
"profile": profile.json(),
"stats": stats.json(),
"notifications": notifications.json(),
}
Pro Tip: Use
async deffor endpoints that do I/O (database, HTTP calls). Use plaindeffor CPU-bound endpoints — FastAPI runs them in a thread pool automatically so they don't block the event loop.
