FastAPI has been the dominant Python API framework since 2022, and in 2026 it's still the right default — Pydantic v2 made it significantly faster, and the async-first design fits modern infrastructure. This guide builds a production-grade API from scratch, including project layout, async database access, auth, and deployment.
What changed in 2026
- Pydantic v2 (shipped 2023, now fully dominant) uses a Rust core and is ~5–50× faster than v1. All FastAPI versions since 0.100 use it by default.
- SQLAlchemy 2.x async is stable and widely used —
async with AsyncSession replaces the old sync ORM pattern.
- Python 3.12/3.13 are the production standard;
asyncio performance improvements are meaningful at scale.
- Litestar (formerly Starlette-based Piccolo API) is the emerging alternative with stricter typing; check it for greenfield projects where FastAPI feels too implicit.
Project structure
app/
main.py # FastAPI() instance + router includes
routers/
users.py
items.py
schemas/
user.py # Pydantic request/response models
models/
user.py # SQLAlchemy ORM models
services/
user_service.py
db/
session.py # AsyncSessionLocal + get_db dependency
core/
config.py # Settings via pydantic-settings
security.py # JWT helpers
Bootstrap
# app/main.py
from fastapi import FastAPI
from app.routers import users, items
app = FastAPI(title="My API", version="1.0.0")
app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
app.include_router(items.router, prefix="/api/v1/items", tags=["items"])
@app.get("/healthz")
async def health():
return {"status": "ok"}
Schemas and a route
# app/schemas/user.py
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
name: str
class UserResponse(BaseModel):
id: int
email: str
name: str
model_config = {"from_attributes": True} # replaces orm_mode in v2
# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.schemas.user import UserCreate, UserResponse
from app.services.user_service import create_user, get_user
router = APIRouter()
@router.post("/", response_model=UserResponse, status_code=201)
async def create(body: UserCreate, db: AsyncSession = Depends(get_db)):
return await create_user(db, body)
@router.get("/{user_id}", response_model=UserResponse)
async def read(user_id: int, db: AsyncSession = Depends(get_db)):
user = await get_user(db, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Async database session dependency
# app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from app.core.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
yield session
Use asyncpg as the driver: DATABASE_URL = "postgresql+asyncpg://user:pass@host/db".
HTTP status code conventions
| Situation |
Status |
| Resource created |
201 Created |
| Validation error (Pydantic) |
422 Unprocessable Entity (auto) |
| Auth missing |
401 Unauthorized |
| Auth present but forbidden |
403 Forbidden |
| Not found |
404 Not Found |
| Duplicate / conflict |
409 Conflict |
FastAPI returns 422 automatically for Pydantic validation failures. Map your own domain errors to 400 or 409 explicitly.
How to pick your Python API framework
| Framework |
Best for |
| FastAPI |
Async APIs, ML endpoints, type-safe new projects |
| Django REST Framework |
Teams already on Django; admin-heavy apps |
| Flask + marshmallow |
Small scripts, simple internal services |
| Litestar |
Stricter typing, built-in DTO layer |
Common mistakes
Synchronous DB calls in async handlers. Blocking calls block the event loop. Use asyncpg or aiosqlite; never call a sync ORM method without run_in_executor.
Returning ORM models directly. SQLAlchemy objects are lazy-loaded; serializing them outside a session raises DetachedInstanceError. Always return a Pydantic response model.
Ignoring model_config = {"from_attributes": True} on response schemas. Without it, UserResponse.model_validate(orm_obj) fails.
Putting config in module-level globals. Use pydantic-settings with a Settings class and inject via Depends(get_settings).
No background task queue. Long-running work (email, PDF export) must be offloaded to Celery or ARQ — do not block request handlers.
What to skip
- Flask for async workloads — Flask's WSGI model doesn't fit native async. Pick FastAPI.
- Mixing sync and async ORM in the same codebase — pick one and stay consistent.
- Manual OpenAPI YAML — FastAPI generates it from your code; only customize what it generates.
FAQ
Should I use SQLAlchemy or Tortoise ORM?
SQLAlchemy 2.x async is more widely understood and has better migration tooling (Alembic). Tortoise is simpler but has a smaller community.
How do I run database migrations?
Alembic with alembic revision --autogenerate and alembic upgrade head. See How to write a database migration in 2026.
Is FastAPI production-ready?
Yes. Uvicorn + Gunicorn (or a container with CMD ["uvicorn", "app.main:app"]) handles production traffic at scale.
How do I add JWT authentication?
Use python-jose or PyJWT to sign and verify tokens. Create a get_current_user dependency that reads Authorization: Bearer <token> and inject it with Depends.
Where to go next
See How to deploy a FastAPI app in 2026, How to write a database migration in 2026, and How to set up Postgres locally in 2026.