# FastAPI Phase 1 Scaffolding — Best Practices

> Compiled May 15, 2026 from analysis of top FastAPI projects:
> - fastapi/full-stack-fastapi-template (43k★)
> - BrunoTanabe/fastapi-clean-architecture-ddd-template (7k★)
> - Real competitor GitHub issues (Debrify, Lumera, Riven, Decypharr, NuvioTV, Stremio v5, Comet)

## Config (pydantic-settings)

- Use `pydantic_settings.BaseSettings` with `model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", env_ignore_empty=True, extra="ignore")`
- Use `@computed_field` for derived values (DB URI, Redis URL) — keeps config DRY
- Use `@model_validator(mode="after")` for cross-field validation (e.g., SECRET_KEY must change in production)
- Parse CORS origins with a `BeforeValidator` that handles both string and list formats
- **Pitfall:** Don't use `os.environ` directly — pydantic-settings handles env_file, type coercion, and defaults

## Database (async SQLAlchemy 2.0)

- **Separate engines:** Sync engine for Alembic migrations, async engine for FastAPI
- Use `async_sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False)` — `expire_on_commit=False` prevents detached instance errors
- Session factory uses `async with PGAsyncSession() as session:` context manager to guarantee close
- **Pitfall:** Never use `session.commit()` in a generator — use `async with` context manager instead
- Pool pre-ping enabled: `pool_pre_ping=True` to detect stale connections

## Security (pwdlib + PyJWT)

- Use `pwdlib.PasswordHash.recommended()` — returns Argon2 by default (OWASP-recommended). No manual hasher construction needed.
- `verify_password()` returns `(is_valid, new_hashed_password)` — re-hashes if algorithm changed
- JWT: Use **PyJWT** (`import jwt`), NOT `python-jose`. PyJWT is the 2026 FastAPI official tutorial standard (latest release Mar 2026, actively maintained).
- Algorithm: HS256, subject is user ID as string, configurable expiry from settings
- Exception handling: `from jwt.exceptions import InvalidTokenError` — catch this for JWT decode failures
- Auth DI: Use `OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")` for standard Bearer token extraction from Authorization header
- **2-layer dependency chain:** `get_current_user` (decode JWT + fetch user) → `get_current_active_user` (check active status)
- **Pitfall:** Don't use `python-jose` — it's last updated May 2025 and FastAPI has migrated to PyJWT
- **Pitfall:** Don't use `passlib` — it's broken on Python 3.13+; `pwdlib` covers both Argon2 and Bcrypt
- **Pitfall:** Don't use domain entities (e.g. `UserCreateEntity`) as FastAPI route parameters — create separate `RegisterRequest` presentation schemas with `Field(min_length=..., max_length=...)` constraints so validation fails fast at the API boundary (422)

## Logging (structlog)

- Use `structlog` with JSON formatting in production, `ConsoleRenderer` in local
- Configure processors: `merge_contextvars` → `add_log_level` → `StackInfoRenderer` → `set_exc_info` → `TimeStamper` → formatter
- Use `structlog.make_filtering_bound_logger()` for log level filtering
- **Pitfall:** Don't use `print()` for logging — structlog gives structured, searchable logs

## Exception Handling

- Register 3 handlers: `RequestValidationError` (422), `StarletteHTTPException` (4xx), `Exception` (500)
- Never expose internal details in 500 responses — return generic "An unexpected error occurred."
- Validation errors: extract field names from `exc.errors()` → `{field: message}` mapping
- **Pitfall:** Always register exception handlers BEFORE including routers

## Middleware

- CORS: configure with specific origins, not wildcard `*`
- Use `app.add_middleware(CORSMiddleware, allow_origins=..., allow_credentials=True, allow_methods=["*"], allow_headers=["*"])`
- **Pitfall:** Don't add CORS middleware after including routers — order matters

## Docker Compose

- Use healthchecks on all services (`pg_isready` for Postgres, `redis-cli ping` for Redis)
- Named volumes for database persistence
- App depends_on with `condition: service_healthy` — not just `service_started`
- Multi-stage Dockerfile with `uv` for dependency management
- **Pitfall:** Don't use `depends_on` without healthcheck conditions — container may start before DB is ready

## Alembic (async)

- Use `async_engine_from_config()` with `poolclass=pool.NullPool` for migrations
- Run migrations via `asyncio.run(run_async_migrations())` in online mode
- Import all models before running migrations so Alembic can detect them for autogenerate
- **Pitfall:** Don't use sync engine for async migrations — Alembic will hang

## Testing (pytest-asyncio)

- Use `asyncio_mode = "auto"` in pytest config
- Session-scoped DB fixture for test database
- Module-scoped TestClient fixture for API tests
- Use `httpx.AsyncClient` for async route tests
- **Pitfall:** Don't mix sync and async fixtures without proper scope management

## pyproject.toml

- Use `uv` for dependency management (faster than pip, lock-file support)
- Configure ruff with `line-length = 120`, select `["E", "F", "I", "N", "W", "UP", "B", "SIM", "RUF"]`
- Configure mypy with `strict = true`
- Configure pytest with `asyncio_mode = "auto"`, `testpaths = ["tests"]`
- **Pitfall:** Don't use `requirements.txt` — uv.lock is the source of truth

## Key Dependencies

```
fastapi[standard] — FastAPI with uvicorn, pydantic, starlette bundled
uvicorn[standard] — ASGI server with hot-reload
sqlalchemy[asyncio] — ORM with async support
asyncpg — PostgreSQL async driver
alembic — Database migrations
redis + aioredis — Caching layer
pydantic + pydantic-settings — Config and validation
pyjwt[crypto] — JWT (PyJWT, NOT python-jose — 2026 standard)
pwdlib[argon2,bcrypt] — Password hashing (use PasswordHash.recommended())
httpx — Async HTTP client (for external API calls + testing)
structlog — Structured logging
tenacity — Retry with exponential backoff
brotli — Compression for stream proxy
```
