| name | fastapi |
| description | FastAPI framework guardrails, patterns, and best practices for AI-assisted development.
Use when working with FastAPI projects, or when the user mentions FastAPI.
Provides async patterns, Pydantic models, dependency injection, and OpenAPI guidelines.
|
| license | MIT |
| metadata | {"author":"samuel","version":"1.0","category":"framework","language":"python","extensions":".py"} |
FastAPI Framework Guide
Applies to: FastAPI 0.100+, Pydantic v2, SQLAlchemy 2.0
Language: Python 3.10+
Type: Async API Framework
Overview
FastAPI is a modern, high-performance web framework for building APIs with Python based on standard type hints. Built on Starlette (web) and Pydantic (validation).
Use FastAPI when:
- Building REST APIs or GraphQL backends
- Need native async/await support
- Want automatic OpenAPI documentation
- Need high performance (comparable to Node.js/Go)
- Want strong type validation with Pydantic
Consider alternatives when:
- Need full-stack with templates (consider Django)
- Building simple scripts (consider Flask)
- Need extensive admin interface (consider Django)
Core Principles
- Type-First: Use type hints everywhere; they drive validation and docs
- Async by Default: Use
async def for I/O-bound endpoints
- Dependency Injection: Use
Depends() for shared logic and resources
- Thin Endpoints: Keep route handlers thin, business logic in services
- Schema Validation: Use Pydantic models for all request/response data
Guardrails
Code Style
- Use
async def for endpoints with I/O operations
- Use
def (sync) only for CPU-bound operations without I/O
- Never use sync database calls in async context
- Always use type hints on function signatures
- Run
ruff check and mypy before committing
API Design
- Version your API:
/api/v1/...
- Use plural nouns for resources:
/users, /products
- Use HTTP methods correctly: GET (read), POST (create), PUT/PATCH (update), DELETE
- Return appropriate status codes (201 for creation, 204 for deletion)
- Always set
response_model on endpoints
Security
- Validate all inputs with Pydantic (automatic with FastAPI)
- Use
OAuth2PasswordBearer for token auth
- Hash passwords with bcrypt (never store plaintext)
- Set CORS origins explicitly (never use
* in production)
- Use
Depends() for auth checks on every protected endpoint
Error Handling
- Use custom exception classes inheriting from
HTTPException
- Register global exception handlers for consistent responses
- Never expose internal errors to clients
- Always wrap database errors with context
Project Structure
myproject/
āāā pyproject.toml
āāā alembic.ini
āāā alembic/
ā āāā versions/
ā āāā env.py
āāā app/
ā āāā __init__.py
ā āāā main.py # Application entry point
ā āāā config.py # Settings (pydantic-settings)
ā āāā database.py # Async SQLAlchemy engine/session
ā āāā dependencies.py # Shared dependencies
ā āāā models/ # SQLAlchemy ORM models
ā ā āāā __init__.py
ā ā āāā base.py # DeclarativeBase, mixins
ā ā āāā user.py
ā āāā schemas/ # Pydantic v2 schemas
ā ā āāā __init__.py
ā ā āāā user.py
ā āāā api/ # API routes
ā ā āāā __init__.py
ā ā āāā deps.py # API-level dependencies (auth)
ā ā āāā v1/
ā ā āāā __init__.py
ā ā āāā router.py # Aggregates all v1 routers
ā ā āāā endpoints/
ā ā āāā users.py
ā ā āāā products.py
ā āāā services/ # Business logic layer
ā ā āāā __init__.py
ā ā āāā user.py
ā āāā repositories/ # Data access layer
ā ā āāā __init__.py
ā ā āāā user.py
ā āāā core/ # Cross-cutting utilities
ā āāā __init__.py
ā āāā security.py # JWT, password hashing
ā āāā exceptions.py # Custom exception classes
āāā tests/
ā āāā conftest.py # Fixtures (async client, db session)
ā āāā test_users.py
ā āāā factories.py
āāā docker-compose.yml
models/ = SQLAlchemy ORM (database shape)
schemas/ = Pydantic (API shape, validation)
services/ = Business logic (orchestration, rules)
repositories/ = Data access (queries, CRUD)
Application Setup
Main Application
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.router import api_router
from app.config import settings
from app.database import engine
from app.models.base import Base
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup and shutdown events."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
await engine.dispose()
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/health")
async def health_check():
return {"status": "healthy"}
Configuration
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True,
)
PROJECT_NAME: str = "FastAPI App"
VERSION: str = "1.0.0"
DEBUG: bool = False
API_V1_STR: str = "/api/v1"
DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost/db"
SECRET_KEY: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
CORS_ORIGINS: list[str] = ["http://localhost:3000"]
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()
Pydantic Schemas (v2)
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserBase(BaseModel):
email: EmailStr
first_name: str | None = None
last_name: str | None = None
class UserCreate(UserBase):
password: str = Field(..., min_length=8, max_length=100)
class UserUpdate(BaseModel):
"""Partial update: all fields optional."""
email: EmailStr | None = None
first_name: str | None = None
last_name: str | None = None
password: str | None = Field(None, min_length=8, max_length=100)
class UserResponse(UserBase):
model_config = ConfigDict(from_attributes=True)
id: UUID
is_active: bool
created_at: datetime
class PaginatedResponse[T](BaseModel):
items: list[T]
total: int
page: int
size: int
pages: int
Key Pydantic v2 patterns:
- Use
ConfigDict(from_attributes=True) instead of class Config: orm_mode = True
- Use
model_dump() / model_validate() instead of .dict() / .from_orm()
- Use
Field(...) for required fields with constraints
- Use
model_dump(exclude_unset=True) for partial updates
Route Definitions
Router Setup
from fastapi import APIRouter
from app.api.v1.endpoints import auth, users, products
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(products.router, prefix="/products", tags=["products"])
Endpoint Pattern
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.database import get_db
from app.models.user import User
from app.schemas.user import UserResponse, PaginatedResponse
from app.services.user import UserService
router = APIRouter()
@router.get("", response_model=PaginatedResponse[UserResponse])
async def list_users(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get paginated list of users."""
service = UserService(db)
skip = (page - 1) * size
users, total = await service.get_multi(skip=skip, limit=size)
return PaginatedResponse(
items=[UserResponse.model_validate(u) for u in users],
total=total,
page=page,
size=size,
pages=(total + size - 1) // size,
)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get user by ID."""
service = UserService(db)
user = await service.get(user_id)
return UserResponse.model_validate(user)
Dependency Injection
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
payload = verify_token(token)
user = await UserRepository(db).get(payload.sub)
if not user or not user.is_active:
raise HTTPException(status_code=403, detail="Inactive or missing user")
return user
async def get_current_superuser(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_superuser:
raise HTTPException(status_code=403, detail="Not enough permissions")
return current_user
DI best practices:
- Use
Depends() for database sessions, auth, services
- Dependencies can depend on other dependencies (chain)
- Use
yield dependencies for setup/teardown (e.g., DB sessions)
- Keep dependency functions small and focused
Async Patterns
Database (Async SQLAlchemy)
from sqlalchemy.ext.asyncio import (
AsyncSession, async_sessionmaker, create_async_engine,
)
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_pre_ping=True,
pool_size=5,
max_overflow=10,
)
AsyncSessionLocal = async_sessionmaker(
engine, class_=AsyncSession,
expire_on_commit=False,
)
Key async rules
- Use
asyncpg driver (not psycopg2) for PostgreSQL
- Use
selectinload() for eager loading relationships (avoids N+1)
- Use
await session.flush() to get IDs without committing
- Use
await session.refresh(obj) after flush to load defaults
Error Handling
from fastapi import HTTPException, status
class AppException(HTTPException):
def __init__(self, detail: str, status_code: int = 500):
super().__init__(status_code=status_code, detail=detail)
class NotFoundException(AppException):
def __init__(self, detail: str = "Not found"):
super().__init__(detail=detail, status_code=status.HTTP_404_NOT_FOUND)
class ConflictException(AppException):
def __init__(self, detail: str = "Conflict"):
super().__init__(detail=detail, status_code=status.HTTP_409_CONFLICT)
class UnauthorizedException(AppException):
def __init__(self, detail: str = "Unauthorized"):
super().__init__(detail=detail, status_code=status.HTTP_401_UNAUTHORIZED)
class ForbiddenException(AppException):
def __init__(self, detail: str = "Forbidden"):
super().__init__(detail=detail, status_code=status.HTTP_403_FORBIDDEN)
Register a global handler in main.py:
from fastapi.responses import JSONResponse
@app.exception_handler(AppException)
async def app_exception_handler(request, exc):
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
Commands Reference
uvicorn app.main:app --reload --port 8000
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
alembic init alembic
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head
alembic downgrade -1
pytest
pytest -v --cov=app --cov-report=html
pytest tests/test_users.py -k "test_register"
ruff check app/
ruff check app/ --fix
mypy app/
Best Practices Summary
Do
- Use Pydantic for all request/response validation
- Use dependency injection for services and sessions
- Keep endpoints thin, logic in services
- Use
async def for I/O operations
- Handle errors with custom exceptions
- Use type hints everywhere
- Write comprehensive async tests with
httpx.AsyncClient
Don't
- Put business logic in endpoints
- Use sync database calls in async context
- Expose internal ORM models directly as responses
- Skip input validation
- Ignore error handling
- Use global mutable state
Advanced Topics
For detailed patterns, full code examples, and advanced usage, see:
- references/patterns.md -- Repository pattern, services, testing, security, middleware, background tasks, WebSocket
External References