| name | best-practices |
| description | Python FastAPI performance optimization and best practices guidelines |
| license | MIT |
| metadata | {"author":"devnogari","project_type":"python-fastapi","rule_count":48,"categories":8} |
Python FastAPI Best Practices
Comprehensive performance optimization and best practices for FastAPI applications. 48 rules across 8 categories based on FastAPI official documentation (benchmark score 96.8), Pydantic V2, and SQLAlchemy async patterns.
Activation Triggers
This skill activates when:
- Writing new endpoints, routers, or dependencies
- Implementing async patterns and database operations
- Optimizing API performance
- Reviewing or refactoring FastAPI code
- Debugging async, validation, or database issues
- Implementing authentication/authorization
- Writing tests for FastAPI applications
Rule Categories
| # | Category | Priority | Prefix | Rule Count | Focus |
|---|
| 1 | Async Patterns | CRITICAL | async- | 6 | Event loop, blocking I/O |
| 2 | Dependency Injection | CRITICAL | di- | 6 | Dependencies, lifecycle, scopes |
| 3 | Pydantic Models | HIGH | pydantic- | 6 | Validation, serialization |
| 4 | Database Patterns | HIGH | db- | 6 | SQLAlchemy async, sessions |
| 5 | Error Handling | MEDIUM | err- | 6 | HTTPException, handlers |
| 6 | Security | MEDIUM | sec- | 6 | Auth, CORS, rate limiting |
| 7 | Performance | MEDIUM | perf- | 6 | Response, caching, background |
| 8 | Testing | LOW | test- | 6 | pytest-asyncio, fixtures |
Quick Reference - All 48 Rules
Async Patterns (CRITICAL) - async-*
| Rule ID | Rule Name | Impact |
|---|
async-block | Never block the event loop | Prevents server freeze |
async-gather | Use asyncio.gather for parallel I/O | 2-10x speedup |
async-timeout | Always set async operation timeouts | Prevents hung requests |
async-context | Use async context managers | Proper resource cleanup |
async-driver | Use async database/HTTP drivers | Full async stack |
async-executor | Run blocking code in thread pool | CPU-bound isolation |
Dependency Injection (CRITICAL) - di-*
| Rule ID | Rule Name | Impact |
|---|
di-depends | Use Depends() for shared logic | Code reuse, testability |
di-yield | Use yield for cleanup | Proper resource lifecycle |
di-cache | Cache expensive dependencies | Reduce redundant computation |
di-scope | Understand dependency scopes | Request vs app lifetime |
di-override | Use dependency_overrides for testing | Clean test isolation |
di-annotated | Use Annotated for cleaner signatures | Type-safe DI |
Pydantic Models (HIGH) - pydantic-*
| Rule ID | Rule Name | Impact |
|---|
pydantic-field | Use Field() with constraints | Automatic validation |
pydantic-validator | Use @field_validator for custom logic | Complex validation |
pydantic-config | Configure model behavior | Performance tuning |
pydantic-orm | Use from_attributes for ORM | Clean serialization |
pydantic-generic | Use Generic models for reuse | DRY response models |
pydantic-alias | Use alias for API compatibility | Clean external APIs |
Database Patterns (HIGH) - db-*
| Rule ID | Rule Name | Impact |
|---|
db-async | Use async SQLAlchemy engine | Non-blocking DB |
db-session | Use async session context manager | Connection safety |
db-n-plus-one | Avoid N+1 with eager loading | Query optimization |
db-transaction | Use explicit transactions | Data consistency |
db-migration | Use Alembic async migrations | Schema management |
db-pool | Configure connection pool properly | Resource efficiency |
Error Handling (MEDIUM) - err-*
| Rule ID | Rule Name | Impact |
|---|
err-http | Use HTTPException with status codes | Proper HTTP semantics |
err-handler | Register global exception handlers | Centralized errors |
err-validation | Handle RequestValidationError | User-friendly messages |
err-middleware | Use middleware for cross-cutting | Consistent error format |
err-logging | Log errors with context | Debugging support |
err-retry | Implement retry with backoff | Resilience |
Security (MEDIUM) - sec-*
| Rule ID | Rule Name | Impact |
|---|
sec-oauth2 | Use OAuth2PasswordBearer | Standard auth flow |
sec-jwt | Implement JWT properly | Stateless auth |
sec-cors | Configure CORS correctly | Cross-origin security |
sec-rate-limit | Implement rate limiting | DoS protection |
sec-input | Validate and sanitize input | Injection prevention |
sec-headers | Set security headers | Browser protection |
Performance (MEDIUM) - perf-*
| Rule ID | Rule Name | Impact |
|---|
perf-response | Use response_model for filtering | Reduce payload size |
perf-cache | Implement caching strategies | Reduce DB load |
perf-background | Use BackgroundTasks for async work | Faster responses |
perf-streaming | Use StreamingResponse for large data | Memory efficiency |
perf-compression | Enable response compression | Network efficiency |
perf-middleware | Order middleware by frequency | Reduce overhead |
Testing (LOW) - test-*
| Rule ID | Rule Name | Impact |
|---|
test-client | Use TestClient or httpx.AsyncClient | Proper API testing |
test-override | Use dependency_overrides | Clean mocking |
test-async | Use pytest-asyncio properly | Async test support |
test-fixture | Create reusable fixtures | Test organization |
test-mock | Mock external services | Test isolation |
test-coverage | Maintain test coverage | Quality assurance |
Detailed Rules by Category
1. Async Patterns (CRITICAL)
async-block - Never Block the Event Loop
Impact: Server freeze, request timeouts, degraded throughput
import time
import requests
@app.get("/users/{user_id}")
async def get_user(user_id: int):
time.sleep(1)
response = requests.get(f"http://api/users/{user_id}")
return response.json()
import asyncio
import httpx
@app.get("/users/{user_id}")
async def get_user(user_id: int):
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
response = await client.get(f"http://api/users/{user_id}")
return response.json()
async-gather - Use asyncio.gather for Parallel I/O
Impact: 2-10x speedup for independent operations
@app.get("/dashboard")
async def get_dashboard(user_id: int):
user = await get_user(user_id)
orders = await get_orders(user_id)
notifications = await get_notifications(user_id)
return {"user": user, "orders": orders, "notifications": notifications}
@app.get("/dashboard")
async def get_dashboard(user_id: int):
user, orders, notifications = await asyncio.gather(
get_user(user_id),
get_orders(user_id),
get_notifications(user_id)
)
return {"user": user, "orders": orders, "notifications": notifications}
async-timeout - Always Set Async Operation Timeouts
Impact: Prevents hung requests and resource exhaustion
@app.get("/external-api")
async def call_external():
async with httpx.AsyncClient() as client:
response = await client.get("http://slow-api.com/data")
return response.json()
from asyncio import timeout
@app.get("/external-api")
async def call_external():
async with httpx.AsyncClient(timeout=10.0) as client:
async with timeout(15.0):
response = await client.get("http://slow-api.com/data")
return response.json()
async-context - Use Async Context Managers
Impact: Proper resource cleanup, connection management
@app.get("/data")
async def get_data():
client = httpx.AsyncClient()
response = await client.get("http://api/data")
return response.json()
@app.get("/data")
async def get_data():
async with httpx.AsyncClient() as client:
response = await client.get("http://api/data")
return response.json()
async-driver - Use Async Database/HTTP Drivers
Impact: Full async stack, maximum concurrency
from sqlalchemy import create_engine
engine = create_engine("postgresql://user:pass@localhost/db")
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
import requests
response = requests.get(url)
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(url)
async-executor - Run Blocking Code in Thread Pool
Impact: Isolates CPU-bound work from async event loop
import hashlib
@app.post("/hash")
async def compute_hash(data: bytes):
result = hashlib.pbkdf2_hmac('sha256', data, b'salt', 100000)
return {"hash": result.hex()}
import asyncio
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
def blocking_hash(data: bytes) -> bytes:
return hashlib.pbkdf2_hmac('sha256', data, b'salt', 100000)
@app.post("/hash")
async def compute_hash(data: bytes):
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(executor, blocking_hash, data)
return {"hash": result.hex()}
2. Dependency Injection (CRITICAL)
di-depends - Use Depends() for Shared Logic
Impact: Code reuse, testability, separation of concerns
@app.get("/items/")
async def get_items(skip: int = 0, limit: int = 10, db: AsyncSession = ...):
pass
@app.get("/users/")
async def get_users(skip: int = 0, limit: int = 10, db: AsyncSession = ...):
pass
from fastapi import Depends, Query
async def pagination(
skip: int = Query(default=0, ge=0),
limit: int = Query(default=10, ge=1, le=100)
) -> dict:
return {"skip": skip, "limit": limit}
@app.get("/items/")
async def get_items(pagination: dict = Depends(pagination)):
pass
@app.get("/users/")
async def get_users(pagination: dict = Depends(pagination)):
pass
di-yield - Use yield for Cleanup
Impact: Proper resource lifecycle, no leaks
async def get_db():
db = SessionLocal()
return db
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
@app.get("/items/")
async def get_items(db: AsyncSession = Depends(get_db)):
pass
di-cache - Cache Expensive Dependencies
Impact: Reduce redundant computation, faster startup
async def get_settings():
return Settings()
from functools import lru_cache
@lru_cache()
def get_settings() -> Settings:
return Settings()
async def get_current_user(token: str = Depends(oauth2_scheme)):
return await decode_token(token)
di-scope - Understand Dependency Scopes
Impact: Correct resource management per lifetime
@lru_cache()
def get_redis_pool() -> redis.ConnectionPool:
return redis.ConnectionPool(host='localhost', port=6379)
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
def get_request_id() -> str:
return str(uuid.uuid4())
di-override - Use dependency_overrides for Testing
Impact: Clean test isolation without modifying code
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
async with test_session() as session:
yield session
await session.rollback()
def test_get_items():
app.dependency_overrides[get_db] = override_get_db
try:
client = TestClient(app)
response = client.get("/items/")
assert response.status_code == 200
finally:
app.dependency_overrides.clear()
di-annotated - Use Annotated for Cleaner Signatures
Impact: Type-safe DI, cleaner function signatures
@app.get("/items/")
async def get_items(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
pagination: dict = Depends(pagination)
):
pass
from typing import Annotated
DBSession = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]
Pagination = Annotated[dict, Depends(pagination)]
@app.get("/items/")
async def get_items(
db: DBSession,
current_user: CurrentUser,
pagination: Pagination
):
pass
3. Pydantic Models (HIGH)
pydantic-field - Use Field() with Constraints
Impact: Automatic validation, clear API contracts
class UserCreate(BaseModel):
email: str
age: int
name: str
from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
email: EmailStr
age: int = Field(ge=0, le=150, description="User age in years")
name: str = Field(min_length=1, max_length=100, pattern=r'^[a-zA-Z\s]+$')
password: str = Field(min_length=8, max_length=100)
model_config = ConfigDict(
str_strip_whitespace=True,
json_schema_extra={
"example": {
"email": "user@example.com",
"age": 25,
"name": "John Doe",
"password": "securepass123"
}
}
)
pydantic-validator - Use @field_validator for Custom Logic
Impact: Complex validation, business rules enforcement
from pydantic import BaseModel, field_validator, model_validator
class OrderCreate(BaseModel):
quantity: int
price: float
discount: float = 0.0
@field_validator('quantity')
@classmethod
def quantity_must_be_positive(cls, v: int) -> int:
if v <= 0:
raise ValueError('quantity must be positive')
return v
@field_validator('discount')
@classmethod
def discount_in_range(cls, v: float) -> float:
if not 0 <= v <= 1:
raise ValueError('discount must be between 0 and 1')
return v
@model_validator(mode='after')
def validate_total(self) -> 'OrderCreate':
total = self.quantity * self.price * (1 - self.discount)
if total < 0:
raise ValueError('total cannot be negative')
return self
pydantic-config - Configure Model Behavior
Impact: Performance tuning, serialization control
from pydantic import BaseModel, ConfigDict
class UserResponse(BaseModel):
id: int
email: str
name: str
created_at: datetime
model_config = ConfigDict(
from_attributes=True,
str_strip_whitespace=True,
use_enum_values=True,
validate_assignment=True,
extra='forbid',
populate_by_name=True,
json_encoders={datetime: lambda v: v.isoformat()}
)
pydantic-orm - Use from_attributes for ORM
Impact: Clean serialization from SQLAlchemy models
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: DBSession):
user = await db.get(User, user_id)
return {
"id": user.id,
"email": user.email,
"name": user.name
}
class UserResponse(BaseModel):
id: int
email: str
name: str
posts: list['PostResponse'] = []
model_config = ConfigDict(from_attributes=True)
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: DBSession):
user = await db.get(User, user_id)
return user
pydantic-generic - Use Generic Models for Reuse
Impact: DRY response models, consistent API format
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar('T')
class PaginatedResponse(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
size: int
pages: int
@classmethod
def create(cls, items: list[T], total: int, page: int, size: int):
return cls(
items=items,
total=total,
page=page,
size=size,
pages=(total + size - 1) // size
)
class APIResponse(BaseModel, Generic[T]):
success: bool = True
data: T | None = None
error: str | None = None
@app.get("/users/", response_model=PaginatedResponse[UserResponse])
async def list_users(pagination: Pagination, db: DBSession):
users, total = await get_users_paginated(db, pagination)
return PaginatedResponse.create(users, total, pagination['skip'], pagination['limit'])
pydantic-alias - Use Alias for API Compatibility
Impact: Clean external APIs, internal naming freedom
from pydantic import BaseModel, Field
class UserCreate(BaseModel):
first_name: str = Field(alias='firstName')
last_name: str = Field(alias='lastName')
email_address: str = Field(alias='emailAddress')
model_config = ConfigDict(
populate_by_name=True,
json_schema_extra={
"example": {
"firstName": "John",
"lastName": "Doe",
"emailAddress": "john@example.com"
}
}
)
class UserResponse(BaseModel):
first_name: str = Field(serialization_alias='firstName')
last_name: str = Field(serialization_alias='lastName')
model_config = ConfigDict(
from_attributes=True,
by_alias=True
)
4. Database Patterns (HIGH)
db-async - Use Async SQLAlchemy Engine
Impact: Non-blocking database operations
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine("postgresql://user:pass@localhost/db")
SessionLocal = sessionmaker(bind=engine)
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
echo=False,
pool_size=5,
max_overflow=10,
pool_pre_ping=True
)
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
db-session - Use Async Session Context Manager
Impact: Connection safety, automatic cleanup
async def get_users():
session = async_session()
users = await session.execute(select(User))
return users.scalars().all()
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@app.get("/users/")
async def get_users(db: DBSession):
result = await db.execute(select(User))
return result.scalars().all()
db-n-plus-one - Avoid N+1 with Eager Loading
Impact: Query optimization, reduced database round trips
@app.get("/users/")
async def get_users_with_posts(db: DBSession):
result = await db.execute(select(User))
users = result.scalars().all()
for user in users:
posts = user.posts
return users
from sqlalchemy.orm import selectinload, joinedload
@app.get("/users/")
async def get_users_with_posts(db: DBSession):
stmt = select(User).options(selectinload(User.posts))
result = await db.execute(stmt)
return result.scalars().unique().all()
stmt = select(User).options(
selectinload(User.posts).selectinload(Post.comments)
)
db-transaction - Use Explicit Transactions
Impact: Data consistency, atomicity
@app.post("/transfer/")
async def transfer_money(from_id: int, to_id: int, amount: float, db: DBSession):
from_account = await db.get(Account, from_id)
from_account.balance -= amount
await db.commit()
to_account = await db.get(Account, to_id)
to_account.balance += amount
await db.commit()
@app.post("/transfer/")
async def transfer_money(from_id: int, to_id: int, amount: float, db: DBSession):
async with db.begin():
from_account = await db.get(Account, from_id)
to_account = await db.get(Account, to_id)
if from_account.balance < amount:
raise HTTPException(400, "Insufficient funds")
from_account.balance -= amount
to_account.balance += amount
db-migration - Use Alembic Async Migrations
Impact: Schema management, version control
from alembic import context
from sqlalchemy.ext.asyncio import create_async_engine
import asyncio
def run_migrations_online():
connectable = create_async_engine(settings.database_url)
def do_run_migrations_sync(connection):
context.configure(
connection=connection,
target_metadata=Base.metadata,
compare_type=True,
compare_server_default=True
)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations():
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations_sync)
await connectable.dispose()
asyncio.run(run_async_migrations())
db-pool - Configure Connection Pool Properly
Impact: Resource efficiency, connection management
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import AsyncAdaptedQueuePool
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
poolclass=AsyncAdaptedQueuePool,
pool_size=5,
max_overflow=10,
pool_timeout=30,
pool_recycle=1800,
pool_pre_ping=True,
echo=False,
echo_pool="debug",
)
@app.on_event("shutdown")
async def shutdown():
await engine.dispose()
5. Error Handling (MEDIUM)
err-http - Use HTTPException with Status Codes
Impact: Proper HTTP semantics, clear error messages
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = await get_item_from_db(item_id)
if not item:
raise Exception("Not found")
from fastapi import HTTPException, status
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = await get_item_from_db(item_id)
if not item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item {item_id} not found",
headers={"X-Error": "Item not found"}
)
return item
err-handler - Register Global Exception Handlers
Impact: Centralized error handling, consistent format
from fastapi import Request
from fastapi.responses import JSONResponse
class AppError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
self.code = code
self.message = message
self.status_code = status_code
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"error": {
"code": exc.code,
"message": exc.message
}
}
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
logger.exception("Unhandled exception")
return JSONResponse(
status_code=500,
content={
"success": False,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred"
}
}
)
err-validation - Handle RequestValidationError
Impact: User-friendly validation messages
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
errors.append({
"field": field,
"message": error["msg"],
"type": error["type"]
})
return JSONResponse(
status_code=422,
content={
"success": False,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": errors
}
}
)
err-middleware - Use Middleware for Cross-Cutting
Impact: Consistent error format across all endpoints
from starlette.middleware.base import BaseHTTPMiddleware
import traceback
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
return response
except Exception as exc:
logger.error(f"Request failed: {traceback.format_exc()}")
return JSONResponse(
status_code=500,
content={
"success": False,
"error": {
"code": "INTERNAL_ERROR",
"message": str(exc) if settings.DEBUG else "Internal server error",
"request_id": request.state.request_id
}
}
)
app.add_middleware(ErrorHandlingMiddleware)
err-logging - Log Errors with Context
Impact: Debugging support, error tracking
import logging
import structlog
from uuid import uuid4
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
)
logger = structlog.get_logger()
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
request_id = str(uuid4())
request.state.request_id = request_id
with structlog.contextvars.bound_contextvars(
request_id=request_id,
method=request.method,
path=request.url.path
):
try:
response = await call_next(request)
logger.info("request_completed", status_code=response.status_code)
return response
except Exception as exc:
logger.exception("request_failed", error=str(exc))
raise
err-retry - Implement Retry with Backoff
Impact: Resilience for transient failures
import asyncio
from functools import wraps
from typing import Type
def retry_async(
max_retries: int = 3,
backoff_factor: float = 2.0,
exceptions: tuple[Type[Exception], ...] = (Exception,)
):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except exceptions as exc:
last_exception = exc
if attempt < max_retries - 1:
wait_time = backoff_factor ** attempt
logger.warning(
f"Retry {attempt + 1}/{max_retries} after {wait_time}s",
error=str(exc)
)
await asyncio.sleep(wait_time)
raise last_exception
return wrapper
return decorator
@retry_async(max_retries=3, exceptions=(httpx.RequestError,))
async def call_external_api(url: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
return response.json()
6. Security (MEDIUM)
sec-oauth2 - Use OAuth2PasswordBearer
Impact: Standard auth flow, OpenAPI integration
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = await authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}
)
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=401,
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 get_user(username)
if user is None:
raise credentials_exception
return user
sec-jwt - Implement JWT Properly
Impact: Stateless authentication, secure token handling
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
SECRET_KEY = settings.SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
REFRESH_TOKEN_EXPIRE_DAYS = 7
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "access"
})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def create_refresh_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
"type": "refresh"
})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
sec-cors - Configure CORS Correctly
Impact: Cross-origin security, frontend integration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://myapp.com",
"https://staging.myapp.com",
"http://localhost:3000" if settings.DEBUG else None
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Total-Count", "X-Page-Count"],
max_age=600,
)
sec-rate-limit - Implement Rate Limiting
Impact: DoS protection, resource management
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/api/items/")
@limiter.limit("100/minute")
async def get_items(request: Request):
return items
def get_user_identifier(request: Request) -> str:
if hasattr(request.state, "user"):
return f"user:{request.state.user.id}"
return get_remote_address(request)
@app.post("/api/expensive-operation/")
@limiter.limit("10/hour", key_func=get_user_identifier)
async def expensive_operation(request: Request):
pass
sec-input - Validate and Sanitize Input
Impact: Injection prevention, data integrity
import re
import bleach
from pydantic import field_validator
class CommentCreate(BaseModel):
content: str = Field(max_length=10000)
@field_validator('content')
@classmethod
def sanitize_content(cls, v: str) -> str:
return bleach.clean(
v,
tags=['p', 'br', 'strong', 'em', 'a'],
attributes={'a': ['href']},
protocols=['http', 'https']
)
class SearchQuery(BaseModel):
q: str = Field(max_length=200)
@field_validator('q')
@classmethod
def validate_query(cls, v: str) -> str:
dangerous_patterns = [
r"'.*--",
r";\s*DROP",
r";\s*DELETE",
r"UNION\s+SELECT"
]
for pattern in dangerous_patterns:
if re.search(pattern, v, re.IGNORECASE):
raise ValueError("Invalid search query")
return v.strip()
sec-headers - Set Security Headers
Impact: Browser protection, security hardening
from starlette.middleware.base import BaseHTTPMiddleware
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
)
if not settings.DEBUG:
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
return response
app.add_middleware(SecurityHeadersMiddleware)
7. Performance (MEDIUM)
perf-response - Use response_model for Filtering
Impact: Reduce payload size, prevent data leaks
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: DBSession):
return await db.get(User, user_id)
class UserResponse(BaseModel):
id: int
email: str
name: str
model_config = ConfigDict(from_attributes=True)
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: DBSession):
user = await db.get(User, user_id)
return user
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
db: DBSession,
include_email: bool = False
) -> UserResponse:
user = await db.get(User, user_id)
response = UserResponse.model_validate(user)
if not include_email:
response.email = None
return response
perf-cache - Implement Caching Strategies
Impact: Reduce database load, faster responses
import redis.asyncio as redis
from functools import wraps
import json
redis_client = redis.from_url("redis://localhost:6379")
def cache_response(ttl: int = 300, key_prefix: str = "cache"):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
cache_key = f"{key_prefix}:{func.__name__}:{hash(str(args) + str(kwargs))}"
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = await func(*args, **kwargs)
await redis_client.setex(
cache_key,
ttl,
json.dumps(result, default=str)
)
return result
return wrapper
return decorator
@app.get("/products/{product_id}")
@cache_response(ttl=600)
async def get_product(product_id: int, db: DBSession):
return await db.get(Product, product_id)
async def invalidate_product_cache(product_id: int):
pattern = f"cache:get_product:*{product_id}*"
async for key in redis_client.scan_iter(match=pattern):
await redis_client.delete(key)
perf-background - Use BackgroundTasks for Async Work
Impact: Faster responses, deferred processing
from fastapi import BackgroundTasks
async def send_email_notification(email: str, message: str):
await email_client.send(email, message)
async def update_analytics(user_id: int, action: str):
await analytics.track(user_id, action)
@app.post("/orders/")
async def create_order(
order: OrderCreate,
background_tasks: BackgroundTasks,
db: DBSession,
current_user: CurrentUser
):
new_order = await create_order_in_db(db, order, current_user.id)
background_tasks.add_task(
send_email_notification,
current_user.email,
f"Order {new_order.id} confirmed!"
)
background_tasks.add_task(
update_analytics,
current_user.id,
"order_created"
)
return new_order
perf-streaming - Use StreamingResponse for Large Data
Impact: Memory efficiency, better UX for large files
from fastapi.responses import StreamingResponse
from typing import AsyncGenerator
async def generate_large_csv(db: DBSession) -> AsyncGenerator[str, None]:
yield "id,name,email\n"
async with db.stream(select(User)) as result:
async for user in result.scalars():
yield f"{user.id},{user.name},{user.email}\n"
@app.get("/export/users")
async def export_users(db: DBSession):
return StreamingResponse(
generate_large_csv(db),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=users.csv"}
)
@app.get("/files/{file_id}")
async def download_file(file_id: str):
file_path = get_file_path(file_id)
async def file_iterator():
async with aiofiles.open(file_path, 'rb') as f:
while chunk := await f.read(64 * 1024):
yield chunk
return StreamingResponse(
file_iterator(),
media_type="application/octet-stream"
)
perf-compression - Enable Response Compression
Impact: Network efficiency, faster transfers
from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=500)
from starlette_compress import CompressMiddleware
app.add_middleware(
CompressMiddleware,
minimum_size=500,
gzip_level=6,
brotli_quality=4,
)
@app.get("/images/{image_id}")
async def get_image(image_id: str):
return FileResponse(
f"images/{image_id}.png",
media_type="image/png",
headers={"Content-Encoding": "identity"}
)
perf-middleware - Order Middleware by Frequency
Impact: Reduce overhead, optimize hot paths
app.add_middleware(LoggingMiddleware)
app.add_middleware(AuthMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(CORSMiddleware)
app.add_middleware(CORSMiddleware, ...)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(GZipMiddleware)
app.add_middleware(RequestIDMiddleware)
app.add_middleware(LoggingMiddleware)
app.add_middleware(ErrorHandlingMiddleware)
app.add_middleware(AuthMiddleware)
8. Testing (LOW)
test-client - Use TestClient or httpx.AsyncClient
Impact: Proper API testing, realistic requests
from fastapi.testclient import TestClient
import pytest
def test_get_items():
client = TestClient(app)
response = client.get("/items/")
assert response.status_code == 200
assert "items" in response.json()
import httpx
import pytest_asyncio
@pytest_asyncio.fixture
async def async_client():
async with httpx.AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.mark.asyncio
async def test_get_items_async(async_client: httpx.AsyncClient):
response = await async_client.get("/items/")
assert response.status_code == 200
test-override - Use dependency_overrides
Impact: Clean mocking, test isolation
import pytest
from unittest.mock import AsyncMock
@pytest_asyncio.fixture
async def test_db():
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async with test_async_session() as session:
yield session
await session.rollback()
async with test_engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
@pytest.fixture
def client(test_db):
async def override_get_db():
yield test_db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as client:
yield client
app.dependency_overrides.clear()
@pytest.fixture
def mock_email_service():
mock = AsyncMock()
mock.send.return_value = True
app.dependency_overrides[get_email_service] = lambda: mock
yield mock
app.dependency_overrides.clear()
test-async - Use pytest-asyncio Properly
Impact: Async test support, event loop management
import pytest
import pytest_asyncio
from httpx import AsyncClient, ASGITransport
pytest_plugins = ('pytest_asyncio',)
@pytest.fixture(scope="session")
def event_loop():
import asyncio
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest_asyncio.fixture(scope="function")
async def async_client():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as client:
yield client
@pytest.mark.asyncio
async def test_create_user(async_client: AsyncClient):
response = await async_client.post(
"/users/",
json={"email": "test@example.com", "password": "secret123"}
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
test-fixture - Create Reusable Fixtures
Impact: Test organization, DRY testing
import pytest
from typing import AsyncGenerator
from faker import Faker
fake = Faker()
@pytest_asyncio.fixture
async def test_user(test_db: AsyncSession) -> User:
"""Create a test user."""
user = User(
email=fake.email(),
name=fake.name(),
password_hash=get_password_hash("testpass123")
)
test_db.add(user)
await test_db.commit()
await test_db.refresh(user)
return user
@pytest_asyncio.fixture
async def auth_headers(test_user: User) -> dict:
"""Get authentication headers for test user."""
token = create_access_token(data={"sub": test_user.email})
return {"Authorization": f"Bearer {token}"}
@pytest_asyncio.fixture
async def test_items(test_db: AsyncSession, test_user: User) -> list[Item]:
"""Create multiple test items."""
items = [
Item(name=fake.word(), price=fake.pyfloat(min_value=1, max_value=100), owner_id=test_user.id)
for _ in range(5)
]
test_db.add_all(items)
await test_db.commit()
return items
@pytest.mark.asyncio
async def test_user_items(async_client: AsyncClient, auth_headers: dict, test_items: list[Item]):
response = await async_client.get("/users/me/items", headers=auth_headers)
assert response.status_code == 200
assert len(response.json()) == 5
test-mock - Mock External Services
Impact: Test isolation, deterministic tests
from unittest.mock import AsyncMock, patch, MagicMock
import pytest
@pytest.fixture
def mock_redis():
"""Mock Redis client."""
mock = AsyncMock()
mock.get.return_value = None
mock.set.return_value = True
mock.delete.return_value = 1
return mock
@pytest.fixture
def mock_http_client():
"""Mock httpx client for external API calls."""
mock = AsyncMock()
mock.get.return_value = MagicMock(
status_code=200,
json=lambda: {"data": "mocked"}
)
return mock
@pytest.mark.asyncio
async def test_cached_endpoint(async_client: AsyncClient, mock_redis: AsyncMock):
with patch("app.dependencies.redis_client", mock_redis):
mock_redis.get.return_value = None
response = await async_client.get("/products/1")
assert response.status_code == 200
mock_redis.set.assert_called_once()
mock_redis.get.return_value = '{"id": 1, "name": "Product"}'
response = await async_client.get("/products/1")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_external_api_failure(async_client: AsyncClient, mock_http_client: AsyncMock):
mock_http_client.get.side_effect = httpx.RequestError("Connection failed")
with patch("app.services.external.http_client", mock_http_client):
response = await async_client.get("/external-data")
assert response.status_code == 503
test-coverage - Maintain Test Coverage
Impact: Quality assurance, confidence in changes
"""
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
addopts = [
"--cov=app",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=80"
]
[tool.coverage.run]
source = ["app"]
omit = ["app/migrations/*", "app/tests/*"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:"
]
"""
import pytest
import coverage
@pytest.fixture(scope="session", autouse=True)
def coverage_setup():
"""Ensure coverage captures async code."""
cov = coverage.Coverage()
cov.start()
yield
cov.stop()
cov.save()
Project Structure
app/
├── main.py # FastAPI app, routers, startup
├── dependencies.py # Shared dependencies (DI)
├── config.py # Settings (pydantic-settings)
├── models/ # SQLAlchemy ORM models
│ ├── __init__.py
│ ├── base.py # Base model class
│ └── user.py
├── schemas/ # Pydantic request/response models
│ ├── __init__.py
│ ├── common.py # Generic models (pagination, etc.)
│ └── user.py
├── routers/ # API routers by domain
│ ├── __init__.py
│ └── users.py
├── services/ # Business logic layer
│ ├── __init__.py
│ └── user_service.py
├── repositories/ # Data access layer
│ ├── __init__.py
│ └── user_repository.py
├── core/
│ ├── security.py # Auth utilities, JWT
│ ├── exceptions.py # Custom exceptions
│ └── middleware.py # Custom middleware
└── tests/
├── conftest.py # Fixtures
├── test_users.py
└── factories/ # Test data factories
Integration Workflow
When implementing FastAPI features:
- Before implementation: Review relevant rule categories
- During development: Apply patterns from quick reference
- Code review: Verify against rule checklist
- Performance issues: Consult detailed rules for solutions
async-* rules → Ensure non-blocking operations
↓
di-* rules → Proper dependency injection
↓
pydantic-* rules → Validation and serialization
↓
db-* rules → Efficient database access
↓
err-* rules → Consistent error handling
↓
sec-* rules → Security hardening
↓
perf-* rules → Performance optimization
↓
test-* rules → Quality assurance
Compliance Checklist
Before submitting FastAPI code: