FastAPI Skill
Purpose: Build production-ready REST APIs with FastAPI
When to use: Creating API endpoints, authentication, middleware, error handling
Project Structure
code
backend/api/
├── main.py # FastAPI app, CORS, lifespan
├── deps.py # Dependency injection
├── middleware.py # Tenant context, logging
└── v1/routers/
├── stores.py
├── products.py
├── forecasts.py
├── alerts.py
└── integrations.py
Core Patterns
1. App Setup
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: init DB pool, Redis
await init_db()
await init_redis()
yield
# Shutdown: close connections
await close_db()
await close_redis()
app = FastAPI(
title="ShelfOps API",
version="1.0.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"],
allow_headers=["*"],
)
2. Pydantic Models (Request/Response)
python
from pydantic import BaseModel, Field
from uuid import UUID
from datetime import datetime
class StoreCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
city: str
state: str = Field(..., min_length=2, max_length=2)
class StoreResponse(BaseModel):
store_id: UUID
customer_id: UUID
name: str
city: str
state: str
created_at: datetime
model_config = {"from_attributes": True}
3. Dependency Injection
python
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
async def get_current_user(
token: str = Depends(oauth2_scheme)
) -> User:
payload = decode_jwt(token)
user = await get_user_by_id(payload["sub"])
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
async def set_tenant_context(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)
):
await db.execute(
text("SET LOCAL app.current_customer_id = :cid"),
{"cid": str(user.customer_id)}
)
return db
4. Router Pattern
python
from fastapi import APIRouter, Depends, HTTPException, Query
router = APIRouter(prefix="/api/v1/stores", tags=["stores"])
@router.get("/", response_model=list[StoreResponse])
async def list_stores(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: AsyncSession = Depends(set_tenant_context),
):
stores = await db.execute(
select(Store).offset(skip).limit(limit)
)
return stores.scalars().all()
@router.post("/", response_model=StoreResponse, status_code=201)
async def create_store(
store: StoreCreate,
db: AsyncSession = Depends(set_tenant_context),
user: User = Depends(get_current_user),
):
db_store = Store(**store.model_dump(), customer_id=user.customer_id)
db.add(db_store)
await db.commit()
await db.refresh(db_store)
return db_store
5. Error Handling
python
from fastapi import Request
from fastapi.responses import JSONResponse
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.detail, "status_code": exc.status_code}
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(f"Unhandled error: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={"error": "Internal server error", "status_code": 500}
)
6. WebSocket (Real-Time Alerts)
python
from fastapi import WebSocket, WebSocketDisconnect
@app.websocket("/ws/alerts/{customer_id}")
async def websocket_alerts(websocket: WebSocket, customer_id: str):
await websocket.accept()
pubsub = redis_client.pubsub()
await pubsub.subscribe(f"alerts:{customer_id}")
try:
async for message in pubsub.listen():
if message["type"] == "message":
await websocket.send_json(json.loads(message["data"]))
except WebSocketDisconnect:
await pubsub.unsubscribe(f"alerts:{customer_id}")
DO / DON'T
DO
- •✅ Use Pydantic for all request/response validation
- •✅ Use dependency injection for DB sessions, auth
- •✅ Use
async deffor all I/O-bound endpoints - •✅ Return proper HTTP status codes (201 for create, 204 for delete)
- •✅ Use
APIRouterfor modular routes - •✅ Auto-generate OpenAPI docs (built-in)
DON'T
- •❌ Put business logic in route handlers (use service layer)
- •❌ Use synchronous DB calls (use async)
- •❌ Skip input validation (Pydantic handles this)
- •❌ Return 200 for everything (use correct HTTP codes)
- •❌ Hardcode secrets (use environment variables)
Last Updated: 2026-02-09