FastAPI Code Review Skill
Overview
This skill provides code review guidelines for FastAPI applications, focusing on async patterns, Pydantic v2 models, dependency injection, and API best practices.
Key Review Areas
1. Pydantic Models
Check for proper model usage:
- •Use Pydantic v2 syntax and features
- •Define proper validation rules
- •Use appropriate field types
- •Separate request/response models when needed
python
# Good: Pydantic v2 model with validation
from pydantic import BaseModel, Field, field_validator, EmailStr
from typing import Annotated
class UserCreate(BaseModel):
email: EmailStr
username: Annotated[str, Field(min_length=3, max_length=50)]
password: Annotated[str, Field(min_length=8)]
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('Username must be alphanumeric')
return v.lower()
class UserResponse(BaseModel):
id: int
email: EmailStr
username: str
model_config = {'from_attributes': True}
# Avoid: Using dict or Any types when structure is known
def bad_endpoint(data: dict): # Loses validation benefits
pass
2. Async Best Practices
Verify async patterns:
- •Use async def for I/O-bound operations
- •Avoid blocking calls in async functions
- •Use proper async database drivers
- •Consider background tasks for long operations
python
# Good: Proper async usage
from sqlalchemy.ext.asyncio import AsyncSession
@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
# Good: Background tasks for slow operations
from fastapi import BackgroundTasks
@app.post("/send-notification")
async def send_notification(
email: str,
background_tasks: BackgroundTasks
):
background_tasks.add_task(send_email, email)
return {"message": "Notification queued"}
# Avoid: Blocking calls in async context
@app.get("/bad")
async def bad_endpoint():
time.sleep(5) # Blocks the event loop!
requests.get("...") # Use httpx instead
3. Dependency Injection
Review dependency patterns:
- •Use Depends for reusable dependencies
- •Implement proper resource cleanup with yield
- •Cache dependencies when appropriate
- •Use sub-dependencies for composition
python
# Good: Database session dependency with cleanup
from contextlib import asynccontextmanager
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# Good: Authentication dependency
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: AsyncSession = Depends(get_db)
) -> 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])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await db.get(User, user_id)
if user is None:
raise credentials_exception
return user
# Good: Composing dependencies
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)]
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
4. API Design
Check API patterns:
- •Use proper HTTP methods and status codes
- •Implement consistent error responses
- •Version APIs when needed
- •Use proper response models
python
# Good: Well-designed endpoints
from fastapi import APIRouter, status
router = APIRouter(prefix="/api/v1/users", tags=["users"])
@router.get("/", response_model=list[UserResponse])
async def list_users(
skip: int = 0,
limit: Annotated[int, Field(le=100)] = 10,
db: AsyncSession = Depends(get_db)
) -> list[User]:
result = await db.execute(
select(User).offset(skip).limit(limit)
)
return result.scalars().all()
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user: UserCreate,
db: AsyncSession = Depends(get_db)
) -> User:
db_user = User(**user.model_dump())
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
await db.delete(user)
await db.commit()
5. Error Handling
Verify error handling:
- •Use HTTPException for API errors
- •Implement custom exception handlers
- •Return consistent error formats
- •Don't expose internal errors to clients
python
# Good: Custom exception handler
from fastapi import Request
from fastapi.responses import JSONResponse
class AppException(Exception):
def __init__(self, status_code: int, detail: str, error_code: str):
self.status_code = status_code
self.detail = detail
self.error_code = error_code
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": exc.error_code,
"detail": exc.detail,
"path": str(request.url)
}
)
# Good: Validation error handler
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": "VALIDATION_ERROR",
"detail": exc.errors()
}
)
# Avoid: Exposing internal errors
@app.get("/bad")
async def bad_endpoint():
try:
# ...
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) # Exposes internals!
6. Security
Review security patterns:
- •Implement proper authentication (OAuth2, JWT)
- •Use CORS middleware correctly
- •Validate and sanitize inputs
- •Rate limit sensitive endpoints
python
# Good: Security configuration
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"], # Specific origins, not "*"
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
# Good: Rate limiting sensitive endpoints
@app.post("/auth/login")
@limiter.limit("5/minute")
async def login(request: Request, credentials: LoginCredentials):
# ...
# Good: Input validation
@app.get("/search")
async def search(
q: Annotated[str, Query(min_length=1, max_length=100)],
page: Annotated[int, Query(ge=1, le=1000)] = 1
):
# q is validated and safe to use
pass
7. Testing
Verify testing practices:
- •Use TestClient for endpoint testing
- •Mock dependencies properly
- •Test validation and error cases
- •Use pytest fixtures for setup
python
# Good: Comprehensive API tests
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
@pytest.fixture
async def async_client(app):
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def auth_headers(async_client, test_user):
response = await async_client.post(
"/auth/login",
json={"email": test_user.email, "password": "testpass"}
)
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.mark.asyncio
async def test_create_user(async_client, auth_headers):
response = await async_client.post(
"/api/v1/users/",
json={"email": "new@example.com", "username": "newuser", "password": "securepass"},
headers=auth_headers
)
assert response.status_code == 201
assert response.json()["email"] == "new@example.com"
@pytest.mark.asyncio
async def test_create_user_invalid_email(async_client, auth_headers):
response = await async_client.post(
"/api/v1/users/",
json={"email": "invalid", "username": "test", "password": "testpass"},
headers=auth_headers
)
assert response.status_code == 422
Common Issues to Flag
- •Blocking calls in async functions (time.sleep, requests, file I/O)
- •Missing response models losing automatic documentation
- •Improper dependency cleanup (missing yield or try/finally)
- •Overly permissive CORS settings
- •Missing input validation on query/path parameters
- •Exposing internal errors to API consumers
- •N+1 queries from lazy loading in ORM
- •Missing rate limiting on authentication endpoints
Review Checklist
- • Pydantic models properly validate input
- • Async functions don't contain blocking calls
- • Dependencies clean up resources properly
- • API responses use proper status codes
- • Error responses are consistent and safe
- • Authentication/authorization implemented correctly
- • CORS configured appropriately
- • Tests cover critical paths