# FastAPI Best Practices !!! info "Primary sources" - [FastAPI deployment](https://fastapi.tiangolo.com/deployment/) - [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/) --- ## App Factory Pattern Always use `create_app()` — it makes the app testable (inject a test `Settings`) and avoids module-level side effects. ```python # src/my_app/main.py from contextlib import asynccontextmanager from fastapi import FastAPI from my_app.config import Settings from my_app.api import health, v1 def create_app(settings: Settings | None = None) -> FastAPI: if settings is None: settings = Settings() @asynccontextmanager async def lifespan(app: FastAPI): # --- startup --- app.state.settings = settings # Open DB pool, warm caches, etc. yield # --- shutdown --- # Close DB pool, flush buffers, etc. app = FastAPI( title=settings.app_name, version="1.0.0", lifespan=lifespan, docs_url="/docs" if settings.debug else None, redoc_url=None, ) app.include_router(health.router) app.include_router(v1.router, prefix="/api/v1") return app # Module-level instance for uvicorn app = create_app() ``` !!! warning "Prefer lifespan handlers" Never use `@app.on_event("startup")` / `@app.on_event("shutdown")`. These are deprecated. The `asynccontextmanager` lifespan is the canonical approach since FastAPI 0.95. --- ## Router Organization ``` src/my_app/api/ ├── __init__.py ├── health.py # GET /healthz — no auth, no versioning ├── deps.py # Shared Depends() factories └── v1/ ├── __init__.py # APIRouter with prefix="/v1" ├── items.py └── users.py ``` Each router file: ```python from fastapi import APIRouter router = APIRouter(prefix="/items", tags=["items"]) @router.get("/") async def list_items() -> list[Item]: ... ``` Root registration: ```python from fastapi import APIRouter from my_app.api.v1 import items, users router = APIRouter() router.include_router(items.router) router.include_router(users.router) ``` --- ## Pydantic Settings (Configuration) ```python # src/my_app/config.py from pydantic_settings import BaseSettings, SettingsConfigDict from functools import lru_cache class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "My App" debug: bool = False host: str = "0.0.0.0" port: int = 8000 log_level: str = "info" # Add DB URL, secret keys, etc. here — never hardcode # database_url: str # required — will raise if missing @lru_cache def get_settings() -> Settings: return Settings() ``` Use `lru_cache` so the `.env` file is read once. In tests, override with: ```python from my_app.config import get_settings from my_app.main import create_app app = create_app(settings=Settings(debug=True, database_url="sqlite://")) ``` **Never** import `settings` as a module-level singleton — it prevents test overrides. --- ## Health Endpoint ```python # src/my_app/api/health.py from fastapi import APIRouter from fastapi.responses import JSONResponse router = APIRouter(tags=["ops"]) @router.get("/healthz", include_in_schema=False) async def health() -> JSONResponse: """Kubernetes/Docker liveness probe. No auth. No versioning.""" return JSONResponse({"status": "ok"}) @router.get("/readyz", include_in_schema=False) async def readiness(request: Request) -> JSONResponse: """Readiness probe — check DB connectivity, etc.""" # Example: await request.app.state.db.execute("SELECT 1") return JSONResponse({"status": "ready"}) ``` Rules: - No authentication required on `/healthz` and `/readyz`. - `/healthz` — liveness: can the process respond? - `/readyz` — readiness: are dependencies available? - Keep them on the root path (not `/api/v1/healthz`). --- ## Dependency Injection Use `Depends()` to share resources from app state: ```python # src/my_app/api/deps.py from fastapi import Depends, Request from my_app.config import Settings def get_settings(request: Request) -> Settings: return request.app.state.settings SettingsDep = Annotated[Settings, Depends(get_settings)] ``` In route handlers: ```python @router.get("/config") async def show_config(settings: SettingsDep) -> dict: return {"debug": settings.debug} ``` --- ## Error Handling Register a global exception handler for unhandled errors: ```python from fastapi import Request from fastapi.responses import JSONResponse @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: # Log the exception — never expose internal details to clients logger.exception("Unhandled error", exc_info=exc) return JSONResponse(status_code=500, content={"detail": "Internal server error"}) ``` --- ## CORS Add CORS middleware only when needed (e.g., browser clients from a different origin): ```python from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, # list from config, never ["*"] in prod allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) ``` --- ## Response Models Always declare `response_model` or return type annotations — FastAPI uses them for OpenAPI docs and response validation: ```python @router.post("/items/", response_model=ItemOut, status_code=201) async def create_item(item: ItemIn) -> ItemOut: ... ``` Use separate `In` / `Out` models when the write shape differs from the read shape (e.g., password hashing, computed fields). --- ## Security Checklist - Never expose `docs_url` in production (set `docs_url=None` when `not settings.debug`). - Validate all user input with Pydantic models — never pass raw request data to DB queries. - Use `SecretStr` for passwords and API keys in `Settings`. - Apply authentication globally via middleware or `app.include_router(..., dependencies=[Depends(verify_token)])`. - Add `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` headers in production middleware.