
Fastapi Advanced
- 18 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Helps with backend & apis tasks.
About
fastapi-advanced is a Claude Code skill for backend & apis. It helps solo builders move faster with AI-assisted coding.
- fastapi-advanced
- Backend & APIs
- AI-coding skill
Fastapi Advanced by the numbers
- 18 all-time installs (skills.sh)
- Ranked #3,472 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill fastapi-advancedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Helps with backend & apis tasks.
Files
FastAPI Advanced Patterns ()
Production-ready FastAPI patterns for modern Python backends.
Lifespan Management ()
Modern Lifespan Context Manager
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
import redis.asyncio as redis
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan with resource management."""
# Startup
app.state.db_engine = create_async_engine(
settings.database_url,
pool_size=5,
max_overflow=10,
)
app.state.redis = redis.from_url(settings.redis_url)
# Health check connections
async with app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
await app.state.redis.ping()
yield # Application runs
# Shutdown
await app.state.db_engine.dispose()
await app.state.redis.close()
app = FastAPI(lifespan=lifespan)Lifespan with Services
from app.services import EmbeddingsService, LLMService
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialize services
app.state.embeddings = EmbeddingsService(
model=settings.embedding_model,
batch_size=100,
)
app.state.llm = LLMService(
providers=["openai", "anthropic"],
default="anthropic",
)
# Warm up models (optional)
await app.state.embeddings.warmup()
yield
# Cleanup
await app.state.embeddings.close()
await app.state.llm.close()Dependency Injection Patterns
Database Session
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import Depends, Request
async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]:
"""Yield database session from app state."""
async with AsyncSession(
request.app.state.db_engine,
expire_on_commit=False,
) as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raiseService Dependencies
from functools import lru_cache
class AnalysisService:
def __init__(
self,
db: AsyncSession,
embeddings: EmbeddingsService,
llm: LLMService,
):
self.db = db
self.embeddings = embeddings
self.llm = llm
def get_analysis_service(
db: AsyncSession = Depends(get_db),
request: Request = None,
) -> AnalysisService:
return AnalysisService(
db=db,
embeddings=request.app.state.embeddings,
llm=request.app.state.llm,
)
@router.post("/analyses")
async def create_analysis(
data: AnalysisCreate,
service: AnalysisService = Depends(get_analysis_service),
):
return await service.create(data)Cached Dependencies
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str
api_key: str
model_config = {"env_file": ".env"}
@lru_cache
def get_settings() -> Settings:
return Settings()
# Usage in dependencies
def get_db_url(settings: Settings = Depends(get_settings)) -> str:
return settings.database_urlAuthenticated User
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security),
db: AsyncSession = Depends(get_db),
) -> User:
token = credentials.credentials
payload = decode_jwt(token)
user = await db.get(User, payload["sub"])
if not user:
raise HTTPException(401, "Invalid credentials")
return user
async def get_admin_user(
user: User = Depends(get_current_user),
) -> User:
if not user.is_admin:
raise HTTPException(403, "Admin access required")
return userMiddleware Patterns
Request ID Middleware
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
app.add_middleware(RequestIDMiddleware)Timing Middleware
import time
from starlette.middleware.base import BaseHTTPMiddleware
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Response-Time"] = f"{duration:.3f}s"
return responseStructured Logging Middleware
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
logger = structlog.get_logger()
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
log = logger.bind(
request_id=getattr(request.state, "request_id", None),
method=request.method,
path=request.url.path,
)
try:
response = await call_next(request)
log.info(
"request_completed",
status_code=response.status_code,
)
return response
except Exception as exc:
log.exception("request_failed", error=str(exc))
raiseCORS Configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
expose_headers=["X-Request-ID", "X-Response-Time"],
)Settings with Pydantic
from pydantic import Field, field_validator, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# Database
database_url: PostgresDsn
db_pool_size: int = Field(default=5, ge=1, le=20)
db_max_overflow: int = Field(default=10, ge=0, le=50)
# Redis
redis_url: str = "redis://localhost:6379"
# API
api_key: str = Field(min_length=32)
debug: bool = False
# LLM
openai_api_key: str | None = None
anthropic_api_key: str | None = None
@field_validator("database_url", mode="before")
@classmethod
def validate_database_url(cls, v: str) -> str:
if v and "+asyncpg" not in v:
return v.replace("postgresql://", "postgresql+asyncpg://")
return v
@property
def async_database_url(self) -> str:
return str(self.database_url)Exception Handlers
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError
from app.core.exceptions import ProblemException
@app.exception_handler(ProblemException)
async def problem_exception_handler(request: Request, exc: ProblemException):
return JSONResponse(
status_code=exc.status_code,
content=exc.to_problem_detail(),
media_type="application/problem+json",
)
@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
return JSONResponse(
status_code=409,
content={
"type": "https://api.example.com/problems/conflict",
"title": "Conflict",
"status": 409,
"detail": "Resource already exists or constraint violated",
},
media_type="application/problem+json",
)Response Optimization
from fastapi.responses import ORJSONResponse
# Use orjson for faster JSON serialization
app = FastAPI(default_response_class=ORJSONResponse)
# Streaming response
from fastapi.responses import StreamingResponse
@router.get("/export")
async def export_data():
async def generate():
async for chunk in fetch_large_dataset():
yield json.dumps(chunk) + "\n"
return StreamingResponse(
generate(),
media_type="application/x-ndjson",
)Health Checks
from fastapi import APIRouter
health_router = APIRouter(tags=["health"])
@health_router.get("/health")
async def health_check(request: Request):
checks = {}
# Database
try:
async with request.app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
checks["database"] = "healthy"
except Exception as e:
checks["database"] = f"unhealthy: {e}"
# Redis
try:
await request.app.state.redis.ping()
checks["redis"] = "healthy"
except Exception as e:
checks["redis"] = f"unhealthy: {e}"
status = "healthy" if all(v == "healthy" for v in checks.values()) else "unhealthy"
return {"status": status, "checks": checks}Anti-Patterns (FORBIDDEN)
# NEVER use global state
db_session = None # Global mutable state!
# NEVER block the event loop
def sync_db_query(): # Blocking in async context!
return session.query(User).all()
# NEVER skip dependency injection
@router.get("/users")
async def get_users():
db = create_session() # Creating session in route!
return db.query(User).all()
# NEVER ignore lifespan cleanup
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.pool = create_pool()
yield
# Missing cleanup! Pool never closedKey Decisions
| Decision | Recommendation |
|---|---|
| Lifespan | Use asynccontextmanager (not events) |
| Dependencies | Class-based services with DI |
| Settings | Pydantic Settings with .env |
| Response | ORJSONResponse for performance |
| Middleware | Order: CORS → RequestID → Timing → Logging |
| Health | Check all critical dependencies |
Available Scripts
- `scripts/create-fastapi-app.md` - Context-aware FastAPI application generator
- Auto-detects: Python version, database type, Redis usage, project structure
- Usage:
/create-fastapi-app [app-name] - Uses
$ARGUMENTSand!commandfor project-specific configuration - Generates production-ready app with detected dependencies
- `assets/fastapi-app-template.py` - Static FastAPI application template
Related Skills
clean-architecture- Service layer patternsdatabase-schema-designer- SQLAlchemy modelsobservability-monitoring- Logging and metrics
Capability Details
lifespan
Keywords: lifespan, startup, shutdown, asynccontextmanager Solves:
- FastAPI startup/shutdown
- Resource management in FastAPI
dependencies
Keywords: dependency injection, Depends, get_db, service dependency Solves:
- FastAPI dependency injection patterns
- Reusable dependencies
middleware
Keywords: middleware, request id, timing, cors, logging middleware Solves:
- Custom FastAPI middleware
- Request/response interceptors
settings
Keywords: settings, pydantic settings, env, configuration Solves:
- FastAPI configuration management
- Environment variables
health-checks
Keywords: health check, readiness, liveness, health endpoint Solves:
- Kubernetes health checks
- Service health monitoring
"""
FastAPI Production Application Template
Production-ready FastAPI application with:
- Lifespan management
- Middleware stack
- Dependency injection
- Error handling
- Health checks
"""
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from functools import lru_cache
import redis.asyncio as redis
import structlog
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from starlette.middleware.base import BaseHTTPMiddleware
# ============================================================================
# Configuration
# ============================================================================
class Settings(BaseSettings):
"""Application settings from environment."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# App
app_name: str = "FastAPI App"
app_version: str = "1.0.0"
debug: bool = False
# Database
database_url: str = "postgresql+asyncpg://user:pass@localhost/db"
db_pool_size: int = Field(default=5, ge=1, le=20)
db_max_overflow: int = Field(default=10, ge=0, le=50)
# Redis
redis_url: str = "redis://localhost:6379"
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
# Auth
jwt_secret: str = "change-me-in-production"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 15
@lru_cache
def get_settings() -> Settings:
"""Cached settings singleton."""
return Settings()
# ============================================================================
# Logging
# ============================================================================
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
)
logger = structlog.get_logger()
# ============================================================================
# Middleware
# ============================================================================
class RequestIDMiddleware(BaseHTTPMiddleware):
"""Add unique request ID to each request."""
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
class TimingMiddleware(BaseHTTPMiddleware):
"""Track request processing time."""
async def dispatch(self, request: Request, call_next):
import time
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Response-Time"] = f"{duration:.4f}s"
request.state.duration = duration
return response
class LoggingMiddleware(BaseHTTPMiddleware):
"""Structured logging for all requests."""
async def dispatch(self, request: Request, call_next):
log = logger.bind(
request_id=getattr(request.state, "request_id", None),
method=request.method,
path=request.url.path,
)
try:
response = await call_next(request)
log.info(
"request_completed",
status_code=response.status_code,
duration=getattr(request.state, "duration", None),
)
return response
except Exception as exc:
log.exception("request_failed", error=str(exc))
raise
# ============================================================================
# Lifespan
# ============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan with resource management."""
settings = get_settings()
logger.info("application_starting", version=settings.app_version)
# STARTUP
# Database
app.state.db_engine = create_async_engine(
settings.database_url,
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_pre_ping=True,
)
async with app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
logger.info("database_connected")
# Redis
app.state.redis = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
)
await app.state.redis.ping()
logger.info("redis_connected")
# Startup time
app.state.started_at = datetime.now(timezone.utc)
logger.info("application_started")
yield
# SHUTDOWN
logger.info("application_stopping")
await app.state.redis.close()
await app.state.db_engine.dispose()
logger.info("application_stopped")
# ============================================================================
# Application
# ============================================================================
app = FastAPI(
title=get_settings().app_name,
version=get_settings().app_version,
lifespan=lifespan,
default_response_class=ORJSONResponse,
)
# ============================================================================
# Middleware Registration (reverse order)
# ============================================================================
# Innermost (runs last)
app.add_middleware(LoggingMiddleware)
app.add_middleware(TimingMiddleware)
app.add_middleware(RequestIDMiddleware)
# Outermost (runs first)
app.add_middleware(
CORSMiddleware,
allow_origins=get_settings().cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Request-ID", "X-Response-Time"],
)
# ============================================================================
# Dependencies
# ============================================================================
async def get_db(request: Request) -> AsyncGenerator[AsyncSession]:
"""Dependency to get database session."""
async with AsyncSession(
request.app.state.db_engine,
expire_on_commit=False,
) as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
async def get_redis(request: Request) -> redis.Redis:
"""Dependency to get Redis client."""
return request.app.state.redis
# ============================================================================
# Exception Handlers
# ============================================================================
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""Handle HTTP exceptions with RFC 9457 format."""
return ORJSONResponse(
status_code=exc.status_code,
content={
"type": f"https://api.example.com/problems/{exc.status_code}",
"title": exc.detail,
"status": exc.status_code,
"instance": request.url.path,
"trace_id": getattr(request.state, "request_id", None),
},
media_type="application/problem+json",
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
"""Handle unexpected exceptions."""
logger.exception(
"unhandled_exception",
request_id=getattr(request.state, "request_id", None),
error=str(exc),
)
return ORJSONResponse(
status_code=500,
content={
"type": "https://api.example.com/problems/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred",
"instance": request.url.path,
"trace_id": getattr(request.state, "request_id", None),
},
media_type="application/problem+json",
)
# ============================================================================
# Routes
# ============================================================================
@app.get("/health")
async def health_check(request: Request):
"""Health check endpoint."""
checks = {}
# Database
try:
async with request.app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
checks["database"] = "healthy"
except Exception as e:
checks["database"] = f"unhealthy: {e}"
# Redis
try:
await request.app.state.redis.ping()
checks["redis"] = "healthy"
except Exception as e:
checks["redis"] = f"unhealthy: {e}"
all_healthy = all(v == "healthy" for v in checks.values())
status_code = 200 if all_healthy else 503
return ORJSONResponse(
status_code=status_code,
content={
"status": "healthy" if all_healthy else "unhealthy",
"checks": checks,
"uptime_seconds": (
datetime.now(timezone.utc) - request.app.state.started_at
).total_seconds(),
},
)
@app.get("/")
async def root():
"""Root endpoint."""
return {
"name": get_settings().app_name,
"version": get_settings().app_version,
"docs": "/docs",
}
# ============================================================================
# Example Resource Routes
# ============================================================================
from pydantic import BaseModel # noqa: E402
class ItemCreate(BaseModel):
name: str
description: str | None = None
class ItemResponse(BaseModel):
id: str
name: str
description: str | None
created_at: datetime
@app.post("/items", response_model=ItemResponse, status_code=201)
async def create_item(
item: ItemCreate,
db: AsyncSession = Depends(get_db),
cache: redis.Redis = Depends(get_redis),
):
"""Create a new item."""
# Example implementation
new_item = ItemResponse(
id=str(uuid.uuid4()),
name=item.name,
description=item.description,
created_at=datetime.now(timezone.utc),
)
# Cache the item
await cache.setex(
f"item:{new_item.id}",
300,
new_item.model_dump_json(),
)
return new_item
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(
item_id: str,
db: AsyncSession = Depends(get_db),
cache: redis.Redis = Depends(get_redis),
):
"""Get an item by ID."""
# Try cache first
cached = await cache.get(f"item:{item_id}")
if cached:
return ItemResponse.model_validate_json(cached)
# Would query database here
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item {item_id} not found",
)
# ============================================================================
# Run
# ============================================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8000,
reload=get_settings().debug,
)
FastAPI Production Checklist
Application Setup
Lifespan Management
- [ ] Use
asynccontextmanagerlifespan (not deprecated events)
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup
yield
# shutdown- [ ] Initialize all connections in lifespan:
- [ ] Database engine with connection pool
- [ ] Redis client
- [ ] Task queue (ARQ/Celery)
- [ ] LLM clients
- [ ] Cleanup all resources on shutdown (reverse order)
- [ ] Verify connections on startup (ping/SELECT 1)
- [ ] Handle graceful shutdown with active connections
Configuration
- [ ] Use Pydantic Settings for configuration:
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")- [ ] Validate settings on startup
- [ ] Use
@lru_cachefor settings singleton - [ ] Don't hardcode secrets
Response Class
- [ ] Use ORJSONResponse for better performance:
app = FastAPI(default_response_class=ORJSONResponse)Middleware Stack
Order (add in reverse)
- [ ] Rate Limiting (innermost)
- [ ] Authentication
- [ ] Logging
- [ ] Timing
- [ ] Request ID
- [ ] CORS (outermost)
Required Middleware
- [ ] CORS: Configure for your domains
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)- [ ] Request ID: Generate unique ID per request
- [ ] Timing: Track response time
- [ ] Logging: Structured request logging
Dependency Injection
Database Session
- [ ] Use async session with proper transaction handling:
async def get_db(request: Request):
async with AsyncSession(request.app.state.db_engine) as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raiseService Dependencies
- [ ] Inject dependencies, don't instantiate in routes
- [ ] Use factories for complex dependencies
- [ ] Consider dependency caching for expensive operations
Error Handling
Exception Handlers
- [ ] Register handler for custom exceptions
- [ ] Register handler for validation errors
- [ ] Register handler for database errors
- [ ] Register catch-all for unexpected errors
RFC 9457 Problem Details
- [ ] Return
application/problem+jsonfor errors - [ ] Include required fields: type, status
- [ ] Include trace ID in error responses
- [ ] Don't leak internal details in production
Security
Authentication
- [ ] Use HTTPBearer for JWT:
security = HTTPBearer()- [ ] Validate tokens in dependency
- [ ] Set appropriate token expiry (15min access, 7d refresh)
- [ ] Use bcrypt for password hashing (cost >= 12)
Input Validation
- [ ] Use Pydantic models for all request bodies
- [ ] Validate path/query parameters
- [ ] Sanitize user input
- [ ] Use
Field()constraints
Headers
- [ ] Add security headers:
- [ ]
X-Content-Type-Options: nosniff - [ ]
X-Frame-Options: DENY - [ ]
Strict-Transport-Security - [ ] Use HTTPS in production
Performance
Async Best Practices
- [ ] Use async database driver (asyncpg)
- [ ] Use async Redis client
- [ ] Don't block event loop with sync operations
- [ ] Use
run_in_executorfor blocking I/O if needed
Connection Pooling
- [ ] Configure database pool size:
create_async_engine(
url,
pool_size=5,
max_overflow=10,
pool_pre_ping=True,
)- [ ] Configure Redis max connections
Caching
- [ ] Cache expensive computations
- [ ] Use proper TTLs
- [ ] Implement cache invalidation
Observability
Logging
- [ ] Use structured logging (structlog)
- [ ] Include request ID in all logs
- [ ] Log at appropriate levels
- [ ] Don't log sensitive data
Metrics
- [ ] Track request latency
- [ ] Track error rates
- [ ] Track cache hit rates
- [ ] Track queue depth
Health Checks
- [ ] Implement
/healthendpoint - [ ] Check all dependencies
- [ ] Return proper status codes:
- [ ] 200 for healthy
- [ ] 503 for unhealthy
Documentation
OpenAPI
- [ ] Add descriptions to all routes
- [ ] Document all response codes
- [ ] Include request/response examples
- [ ] Tag routes appropriately
API Info
- [ ] Set app title and description
- [ ] Set version
- [ ] Configure docs URL
Testing
Test Configuration
- [ ] Use test database
- [ ] Mock external services
- [ ] Use pytest-asyncio
Test Coverage
- [ ] Unit tests for business logic
- [ ] Integration tests for routes
- [ ] Test error handling
- [ ] Test authentication
Deployment
Docker
- [ ] Multi-stage build
- [ ] Non-root user
- [ ] Health check in Dockerfile
Kubernetes
- [ ] Readiness probe
- [ ] Liveness probe
- [ ] Resource limits
- [ ] Horizontal pod autoscaler
Environment Variables
- [ ] All secrets from environment
- [ ] Different configs per environment
- [ ] Validate required variables on startup
Quick Reference
| Concern | Solution |
|---|---|
| Startup/Shutdown | asynccontextmanager lifespan |
| Config | Pydantic Settings |
| DB Session | Dependency with context manager |
| Auth | HTTPBearer + JWT validation |
| Errors | RFC 9457 Problem Details |
| Logging | Structlog with request ID |
| Response | ORJSONResponse |
| Middleware | CORS → RequestID → Timing → Logging → Auth → RateLimit |
FastAPI Lifespan Management
Complete examples for managing application lifecycle in FastAPI.
Basic Lifespan
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Application lifespan context manager.
Code before yield runs on startup.
Code after yield runs on shutdown.
"""
# STARTUP
print("Application starting...")
app.state.started_at = datetime.now(timezone.utc)
yield # Application runs here
# SHUTDOWN
print("Application shutting down...")
app = FastAPI(lifespan=lifespan)Full Production Lifespan
from contextlib import asynccontextmanager
from datetime import datetime, timezone
import structlog
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import text
import redis.asyncio as redis
from app.core.config import settings
logger = structlog.get_logger()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Production lifespan with full resource management.
Initializes:
- Database connection pool
- Redis connection
- Background task queue
- LLM clients
"""
logger.info("application_starting", version=settings.app_version)
# =====================================================================
# STARTUP
# =====================================================================
# 1. Database Engine
logger.info("initializing_database")
app.state.db_engine = create_async_engine(
settings.database_url,
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_pre_ping=True, # Verify connections
echo=settings.debug,
)
# Verify database connection
async with app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
logger.info("database_connected")
# 2. Redis
logger.info("initializing_redis")
app.state.redis = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
max_connections=20,
)
await app.state.redis.ping()
logger.info("redis_connected")
# 3. Task Queue (ARQ)
logger.info("initializing_task_queue")
from arq import create_pool
from arq.connections import RedisSettings
app.state.task_queue = await create_pool(
RedisSettings.from_dsn(settings.redis_url)
)
logger.info("task_queue_connected")
# 4. LLM Clients
logger.info("initializing_llm_clients")
from app.services.llm import LLMService
app.state.llm = LLMService(
openai_key=settings.openai_api_key,
anthropic_key=settings.anthropic_api_key,
)
logger.info("llm_clients_initialized")
# 5. Embeddings Service
logger.info("initializing_embeddings")
from app.services.embeddings import EmbeddingsService
app.state.embeddings = EmbeddingsService(
model=settings.embedding_model,
)
# Warmup embedding model
await app.state.embeddings.embed("warmup")
logger.info("embeddings_initialized")
# Record startup time
app.state.started_at = datetime.now(timezone.utc)
logger.info("application_started")
# =====================================================================
# APPLICATION RUNS HERE
# =====================================================================
yield
# =====================================================================
# SHUTDOWN
# =====================================================================
logger.info("application_stopping")
# Close in reverse order
logger.info("closing_embeddings")
await app.state.embeddings.close()
logger.info("closing_llm_clients")
await app.state.llm.close()
logger.info("closing_task_queue")
await app.state.task_queue.close()
logger.info("closing_redis")
await app.state.redis.close()
logger.info("closing_database")
await app.state.db_engine.dispose()
logger.info("application_stopped")
# Create app with lifespan
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
lifespan=lifespan,
)Accessing App State in Routes
from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
async def get_db(request: Request) -> AsyncSession:
"""Dependency to get database session."""
async with AsyncSession(
request.app.state.db_engine,
expire_on_commit=False,
) as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
async def get_redis(request: Request):
"""Dependency to get Redis client."""
return request.app.state.redis
async def get_task_queue(request: Request):
"""Dependency to get task queue."""
return request.app.state.task_queue
@router.get("/analyses/{id}")
async def get_analysis(
id: str,
db: AsyncSession = Depends(get_db),
redis=Depends(get_redis),
):
# Try cache first
cached = await redis.get(f"analysis:{id}")
if cached:
return json.loads(cached)
# Query database
analysis = await db.get(AnalysisModel, id)
return analysis
@router.post("/analyses")
async def create_analysis(
request: CreateAnalysisRequest,
db: AsyncSession = Depends(get_db),
queue=Depends(get_task_queue),
):
# Create record
analysis = AnalysisModel(**request.dict())
db.add(analysis)
await db.commit()
# Enqueue background processing
await queue.enqueue_job("process_analysis", analysis_id=str(analysis.id))
return analysisHealth Check Using App State
@router.get("/health")
async def health_check(request: Request):
"""Check health of all dependencies."""
checks = {}
# Database
try:
async with request.app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
checks["database"] = "healthy"
except Exception as e:
checks["database"] = f"unhealthy: {e}"
# Redis
try:
await request.app.state.redis.ping()
checks["redis"] = "healthy"
except Exception as e:
checks["redis"] = f"unhealthy: {e}"
# LLM
try:
if request.app.state.llm.is_available():
checks["llm"] = "healthy"
else:
checks["llm"] = "unhealthy: no providers available"
except Exception as e:
checks["llm"] = f"unhealthy: {e}"
# Overall status
all_healthy = all(v == "healthy" for v in checks.values())
status = "healthy" if all_healthy else "degraded"
return {
"status": status,
"checks": checks,
"uptime_seconds": (datetime.now(timezone.utc) - request.app.state.started_at).total_seconds(),
}Graceful Shutdown
import signal
import asyncio
from contextlib import asynccontextmanager
# Track active connections
active_connections: set = set()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Setup signal handlers for graceful shutdown
def handle_shutdown(signum, frame):
logger.info("shutdown_signal_received", signal=signum)
asyncio.create_task(graceful_shutdown())
signal.signal(signal.SIGTERM, handle_shutdown)
signal.signal(signal.SIGINT, handle_shutdown)
# Startup
app.state.shutting_down = False
yield
# Shutdown
logger.info("waiting_for_active_connections", count=len(active_connections))
# Wait for active requests (up to 30 seconds)
for _ in range(30):
if not active_connections:
break
await asyncio.sleep(1)
logger.info("shutdown_complete")
async def graceful_shutdown():
"""Initiate graceful shutdown."""
app.state.shutting_down = True
logger.info("graceful_shutdown_initiated")
# Middleware to track active connections
@app.middleware("http")
async def track_connections(request: Request, call_next):
if app.state.shutting_down:
return JSONResponse(
status_code=503,
content={"detail": "Server is shutting down"},
)
connection_id = id(request)
active_connections.add(connection_id)
try:
return await call_next(request)
finally:
active_connections.discard(connection_id)Testing with Lifespan
# tests/conftest.py
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from app.main import app
@pytest.fixture
async def client():
"""Test client with mocked dependencies."""
# Override app state for testing
app.state.db_engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
echo=True,
)
app.state.redis = FakeRedis()
app.state.task_queue = FakeTaskQueue()
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
@pytest.fixture
async def db_session(client):
"""Get database session for tests."""
async with AsyncSession(app.state.db_engine) as session:
yield sessionEnvironment-Specific Lifespan
from app.core.config import settings
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifespan with environment-specific behavior."""
if settings.environment == "test":
# Minimal setup for tests
app.state.db_engine = create_async_engine("sqlite+aiosqlite:///:memory:")
app.state.redis = FakeRedis()
yield
return
if settings.environment == "development":
# Development setup
app.state.db_engine = create_async_engine(
settings.database_url,
echo=True, # SQL logging
)
app.state.redis = redis.from_url(settings.redis_url)
yield
await app.state.redis.close()
await app.state.db_engine.dispose()
return
# Production setup (full initialization)
# ... full production initialization code ...
yield
# ... full cleanup code ...FastAPI Middleware Stack
Complete guide to middleware ordering and implementation in FastAPI.
Middleware Execution Order
REQUEST RESPONSE
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 1. CORS Middleware (outermost) │
│ - Handles preflight requests │
│ - Adds CORS headers to response │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 2. Request ID Middleware │
│ - Generates/extracts request ID │
│ - Adds to response headers │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 3. Timing Middleware │
│ - Records start time │
│ - Calculates duration │
│ - Adds X-Response-Time header │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 4. Logging Middleware │
│ - Logs request details │
│ - Logs response status │
│ - Uses request ID for correlation │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 5. Authentication Middleware (optional) │
│ - Validates JWT/API key │
│ - Sets request.state.user │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ 6. Rate Limit Middleware │
│ - Checks rate limits │
│ - Returns 429 if exceeded │
│ - Adds rate limit headers │
└──────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────────────────────────┐
│ ROUTE HANDLER │
│ (Your endpoint code) │
└──────────────────────────────────────────────────────────────┘Note: Middleware added LAST executes FIRST (wraps outer).
Middleware Registration Order
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 6. Rate Limit (added last, runs closest to route)
app.add_middleware(RateLimitMiddleware)
# 5. Authentication (optional)
app.add_middleware(AuthMiddleware)
# 4. Logging
app.add_middleware(LoggingMiddleware)
# 3. Timing
app.add_middleware(TimingMiddleware)
# 2. Request ID
app.add_middleware(RequestIDMiddleware)
# 1. CORS (added first, runs first/last)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Core Middleware Implementations
Request ID Middleware
import uuid
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class RequestIDMiddleware(BaseHTTPMiddleware):
"""Add unique request ID to each request."""
async def dispatch(self, request: Request, call_next):
# Get from header or generate new
request_id = request.headers.get(
"X-Request-ID",
str(uuid.uuid4()),
)
# Store in request state
request.state.request_id = request_id
# Call next middleware/route
response = await call_next(request)
# Add to response headers
response.headers["X-Request-ID"] = request_id
return responseTiming Middleware
import time
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class TimingMiddleware(BaseHTTPMiddleware):
"""Track request processing time."""
async def dispatch(self, request: Request, call_next):
start_time = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start_time
response.headers["X-Response-Time"] = f"{duration:.4f}s"
# Store for logging middleware
request.state.duration = duration
return responseLogging Middleware
import structlog
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
logger = structlog.get_logger()
class LoggingMiddleware(BaseHTTPMiddleware):
"""Structured logging for all requests."""
async def dispatch(self, request: Request, call_next):
# Bind request context
log = logger.bind(
request_id=getattr(request.state, "request_id", None),
method=request.method,
path=request.url.path,
client_ip=request.client.host if request.client else None,
)
try:
response = await call_next(request)
log.info(
"request_completed",
status_code=response.status_code,
duration=getattr(request.state, "duration", None),
)
return response
except Exception as exc:
log.exception("request_failed", error=str(exc))
raiseCORS Configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
# Production: specify exact origins
allow_origins=[
"https://app.example.com",
"https://admin.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
expose_headers=[
"X-Request-ID",
"X-Response-Time",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
],
max_age=600, # Preflight cache 10 minutes
)Advanced Patterns
Conditional Middleware
class ConditionalMiddleware(BaseHTTPMiddleware):
"""Middleware that only applies to certain paths."""
def __init__(self, app, paths: list[str] = None, exclude_paths: list[str] = None):
super().__init__(app)
self.paths = paths or []
self.exclude_paths = exclude_paths or []
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Skip excluded paths
if any(path.startswith(p) for p in self.exclude_paths):
return await call_next(request)
# Only apply to specific paths if defined
if self.paths and not any(path.startswith(p) for p in self.paths):
return await call_next(request)
# Apply middleware logic
return await self._apply_middleware(request, call_next)
async def _apply_middleware(self, request: Request, call_next):
# Your middleware logic here
return await call_next(request)Error Handling Middleware
from fastapi.responses import JSONResponse
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
"""Catch unhandled exceptions and return proper responses."""
async def dispatch(self, request: Request, call_next):
try:
return await call_next(request)
except Exception as exc:
logger.exception(
"unhandled_exception",
request_id=getattr(request.state, "request_id", None),
path=request.url.path,
)
return JSONResponse(
status_code=500,
content={
"type": "https://api.example.com/problems/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred",
"request_id": getattr(request.state, "request_id", None),
},
media_type="application/problem+json",
)Request Body Caching
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class BodyCacheMiddleware(BaseHTTPMiddleware):
"""Cache request body for multiple reads."""
async def dispatch(self, request: Request, call_next):
# Only cache for methods with body
if request.method in ("POST", "PUT", "PATCH"):
body = await request.body()
request.state.body = body
# Create new receive that returns cached body
async def receive():
return {"type": "http.request", "body": body}
request._receive = receive
return await call_next(request)Performance Considerations
Async vs Sync Middleware
# GOOD: Async middleware (non-blocking)
class AsyncMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
await asyncio.sleep(0) # Async operation
return await call_next(request)
# BAD: Sync operations in async middleware
class BadMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
time.sleep(1) # BLOCKS EVENT LOOP!
return await call_next(request)Middleware vs Dependencies
| Middleware | Dependencies |
|---|---|
| Runs on ALL requests | Runs on specific routes |
| No access to path params | Access to path params |
| Before route matching | After route matching |
| For cross-cutting concerns | For route-specific logic |
Use Middleware for:
- Request ID generation
- Logging
- CORS
- Timing
Use Dependencies for:
- Authentication
- Rate limiting per endpoint
- Request validation
- Database sessions
Testing Middleware
# tests/test_middleware.py
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_request_id_generated(client: AsyncClient):
response = await client.get("/health")
assert "X-Request-ID" in response.headers
# Should be valid UUID
import uuid
uuid.UUID(response.headers["X-Request-ID"])
@pytest.mark.asyncio
async def test_request_id_preserved(client: AsyncClient):
custom_id = "my-custom-id-123"
response = await client.get(
"/health",
headers={"X-Request-ID": custom_id},
)
assert response.headers["X-Request-ID"] == custom_id
@pytest.mark.asyncio
async def test_timing_header_present(client: AsyncClient):
response = await client.get("/health")
assert "X-Response-Time" in response.headers
# Should be a valid duration
duration = float(response.headers["X-Response-Time"].rstrip("s"))
assert duration > 0Related Files
- See
examples/fastapi-middleware.mdfor complete examples - See
scripts/middleware-stack.pyfor copy-paste template - See SKILL.md for lifespan and dependencies
Create FastAPI app: $ARGUMENTS
Project Context (Auto-Detected)
- Python Version: !
python --version 2>/dev/null || echo "Python 3.x" - Project Root: !
basename $(git rev-parse --show-toplevel 2>/dev/null) || echo "project" - Database: !
grep -r "sqlalchemy\|asyncpg\|psycopg" pyproject.toml requirements.txt 2>/dev/null | head -1 | grep -oE 'sqlalchemy|asyncpg|psycopg' || echo "Not detected" - Redis: !
grep -r "redis" pyproject.toml requirements.txt 2>/dev/null | head -1 | grep -oE 'redis' || echo "Not detected" - Existing Structure: !
ls -la 2>/dev/null | grep -E 'app|src|backend' | head -3 | awk '{print $NF}' || echo "No app/src/backend found" - Current Dependencies: !
grep -r "fastapi\|uvicorn\|pydantic" pyproject.toml requirements.txt 2>/dev/null | head -3 || echo "No FastAPI deps found"
FastAPI Application Template
Generate a production-ready FastAPI application with the following structure:
"""
$ARGUMENTS - FastAPI Production Application
Generated: !`date +%Y-%m-%d`
Python: !`python --version 2>/dev/null | cut -d' ' -f2 || echo "3.x"`
"""
import uuid
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from functools import lru_cache
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo "import redis.asyncio as redis" || echo "# Redis not detected - install redis if needed"`
import structlog
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import ORJSONResponse
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo "from sqlalchemy import text" || echo "# SQLAlchemy not detected"`
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo "from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine" || echo "# SQLAlchemy not detected"`
from starlette.middleware.base import BaseHTTPMiddleware
# ============================================================================
# Configuration
# ============================================================================
class Settings(BaseSettings):
"""Application settings from environment."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# App
app_name: str = "$ARGUMENTS"
app_version: str = "1.0.0"
debug: bool = False
# Database
!`grep -q "sqlalchemy\|asyncpg" pyproject.toml requirements.txt 2>/dev/null && echo ' database_url: str = "postgresql+asyncpg://user:pass@localhost/db"' || echo ' # database_url: str = "postgresql+asyncpg://user:pass@localhost/db" # Uncomment if using database"'`
db_pool_size: int = Field(default=5, ge=1, le=20)
db_max_overflow: int = Field(default=10, ge=0, le=50)
# Redis
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo ' redis_url: str = "redis://localhost:6379"' || echo ' # redis_url: str = "redis://localhost:6379" # Uncomment if using Redis"'`
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
# Auth
jwt_secret: str = "change-me-in-production"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 15
@lru_cache
def get_settings() -> Settings:
"""Cached settings singleton."""
return Settings()
# ============================================================================
# Logging
# ============================================================================
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
)
logger = structlog.get_logger()
# ============================================================================
# Middleware
# ============================================================================
class RequestIDMiddleware(BaseHTTPMiddleware):
"""Add unique request ID to each request."""
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
class TimingMiddleware(BaseHTTPMiddleware):
"""Track request processing time."""
async def dispatch(self, request: Request, call_next):
import time
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Response-Time"] = f"{duration:.4f}s"
request.state.duration = duration
return response
class LoggingMiddleware(BaseHTTPMiddleware):
"""Structured logging for all requests."""
async def dispatch(self, request: Request, call_next):
log = logger.bind(
request_id=getattr(request.state, "request_id", None),
method=request.method,
path=request.url.path,
)
try:
response = await call_next(request)
log.info(
"request_completed",
status_code=response.status_code,
duration=getattr(request.state, "duration", None),
)
return response
except Exception as exc:
log.exception("request_failed", error=str(exc))
raise
# ============================================================================
# Lifespan
# ============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan with resource management."""
settings = get_settings()
logger.info("application_starting", version=settings.app_version)
# STARTUP
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo ' # Database
app.state.db_engine = create_async_engine(
settings.database_url,
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_pre_ping=True,
)
async with app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
logger.info("database_connected")' || echo ' # Database setup skipped - SQLAlchemy not detected'`
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo ' # Redis
app.state.redis = redis.from_url(
settings.redis_url,
encoding="utf-8",
decode_responses=True,
)
await app.state.redis.ping()
logger.info("redis_connected")' || echo ' # Redis setup skipped - Redis not detected'`
# Startup time
app.state.started_at = datetime.now(timezone.utc)
logger.info("application_started")
yield
# SHUTDOWN
logger.info("application_stopping")
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo ' await app.state.redis.close()' || echo ' # Redis cleanup skipped'`
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo ' await app.state.db_engine.dispose()' || echo ' # Database cleanup skipped'`
logger.info("application_stopped")
# ============================================================================
# Application
# ============================================================================
app = FastAPI(
title=get_settings().app_name,
version=get_settings().app_version,
lifespan=lifespan,
default_response_class=ORJSONResponse,
)
# ============================================================================
# Middleware Registration (reverse order)
# ============================================================================
# Innermost (runs last)
app.add_middleware(LoggingMiddleware)
app.add_middleware(TimingMiddleware)
app.add_middleware(RequestIDMiddleware)
# Outermost (runs first)
app.add_middleware(
CORSMiddleware,
allow_origins=get_settings().cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Request-ID", "X-Response-Time"],
)
# ============================================================================
# Dependencies
# ============================================================================
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo 'async def get_db(request: Request) -> AsyncGenerator[AsyncSession]:
"""Dependency to get database session."""
async with AsyncSession(
request.app.state.db_engine,
expire_on_commit=False,
) as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise' || echo '# Database dependency skipped - SQLAlchemy not detected'`
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo 'async def get_redis(request: Request) -> redis.Redis:
"""Dependency to get Redis client."""
return request.app.state.redis' || echo '# Redis dependency skipped - Redis not detected'`
# ============================================================================
# Exception Handlers
# ============================================================================
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""Handle HTTP exceptions with RFC 9457 format."""
return ORJSONResponse(
status_code=exc.status_code,
content={
"type": f"https://api.example.com/problems/{exc.status_code}",
"title": exc.detail,
"status": exc.status_code,
"instance": request.url.path,
"trace_id": getattr(request.state, "request_id", None),
},
media_type="application/problem+json",
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
"""Handle unexpected exceptions."""
logger.exception(
"unhandled_exception",
request_id=getattr(request.state, "request_id", None),
error=str(exc),
)
return ORJSONResponse(
status_code=500,
content={
"type": "https://api.example.com/problems/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred",
"instance": request.url.path,
"trace_id": getattr(request.state, "request_id", None),
},
media_type="application/problem+json",
)
# ============================================================================
# Routes
# ============================================================================
@app.get("/health")
async def health_check(request: Request):
"""Health check endpoint."""
checks = {}
# Database
!`grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo ' try:
async with request.app.state.db_engine.connect() as conn:
await conn.execute(text("SELECT 1"))
checks["database"] = "healthy"
except Exception as e:
checks["database"] = f"unhealthy: {e}"' || echo ' checks["database"] = "not configured"'`
# Redis
!`grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo ' try:
await request.app.state.redis.ping()
checks["redis"] = "healthy"
except Exception as e:
checks["redis"] = f"unhealthy: {e}"' || echo ' checks["redis"] = "not configured"'`
all_healthy = all(v == "healthy" for v in checks.values())
status_code = 200 if all_healthy else 503
return ORJSONResponse(
status_code=status_code,
content={
"status": "healthy" if all_healthy else "unhealthy",
"checks": checks,
"uptime_seconds": (
datetime.now(timezone.utc) - request.app.state.started_at
).total_seconds(),
},
)
@app.get("/")
async def root():
"""Root endpoint."""
return {
"name": get_settings().app_name,
"version": get_settings().app_version,
"docs": "/docs",
}
# ============================================================================
# Run
# ============================================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8000,
reload=get_settings().debug,
)Next Steps
1. Save this as app.py or main.py in your project 2. Install dependencies: pip install fastapi uvicorn structlog orjson !grep -q "sqlalchemy" pyproject.toml requirements.txt 2>/dev/null && echo "3. Database is configured - ensure PostgreSQL is running" || echo "3. Add SQLAlchemy if you need database: pip install sqlalchemy asyncpg" !grep -q "redis" pyproject.toml requirements.txt 2>/dev/null && echo "4. Redis is configured - ensure Redis is running" || echo "4. Add Redis if you need caching: pip install redis" 5. Run: uvicorn app:app --reload