# Python Web Stack Standards ## Stack - **API:** FastAPI (async) - **ORM:** async SQLAlchemy 2.0 with ORM models (not raw queries) - **Database:** MySQL 8 - **Runtime:** Docker Compose - **Reverse proxy:** Nginx - **Container management:** Portainer ## Project Layout ``` project-name/ ├── docker-compose.yml ├── .env.example # Template — always include this, never commit .env ├── .gitignore ├── README.md ├── nginx/ │ └── default.conf └── backend/ ├── Dockerfile ├── requirements.txt └── app/ ├── main.py # App entry point, router registration, startup events ├── config.py # Settings loaded from environment via pydantic-settings ├── database.py # Async SQLAlchemy engine and session factory ├── dependencies.py # Shared FastAPI dependencies (auth, db session, etc.) ├── models/ # SQLAlchemy ORM table definitions ├── schemas/ # Pydantic request/response models ├── routers/ # API endpoint handlers, one file per resource └── utils/ # Shared helpers ``` ## Configuration & Secrets - All secrets and environment-specific values go in `.env` — never hardcode them - Always provide `.env.example` with placeholder values and comments explaining each variable - Load settings via `pydantic-settings`: ```python # app/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): db_host: str db_port: int = 3306 db_name: str db_user: str db_password: str secret_key: str class Config: env_file = ".env" settings = Settings() ``` ## Database (SQLAlchemy 2.0 Async) - Use async engine and session factory - Define models with `DeclarativeBase` - One model per file in `models/` ```python # app/database.py from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.orm import DeclarativeBase engine = create_async_engine(DATABASE_URL, echo=False) AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False) class Base(DeclarativeBase): pass ``` - Inject DB sessions via FastAPI dependencies (not global sessions) - Use `async with session.begin()` for transactions ## Models - One file per table in `models/` - Use typed columns with SQLAlchemy 2.0 `Mapped` syntax: ```python from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(unique=True) email: Mapped[str] ``` ## Schemas (Pydantic) - Separate schemas for Create, Update, and Response — don't reuse the same schema for all three - Response schemas should never expose password hashes or internal fields ```python class UserCreate(BaseModel): ... class UserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) ``` ## Routers - One router file per resource (e.g., `routers/users.py`, `routers/auth.py`) - Register all routers in `main.py` with a consistent prefix and tags ## Docker Compose - Use named volumes for persistent data (database, uploads) - Always define a `healthcheck` for the database service - Use `depends_on` with `condition: service_healthy` for the backend - Keep the Nginx config in a `nginx/` folder and mount it as a volume ```yaml services: db: image: mysql:8 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 backend: build: ./backend depends_on: db: condition: service_healthy ``` ## Testing - Use **pytest** with **pytest-asyncio** for async tests - Always provide a test for each router endpoint at minimum (happy path + 1 failure case) - Use a separate test database — never run tests against the real DB - Run with: `pytest -v` ## Comments - Docstrings on all functions that aren't self-explanatory - Comment the **why** for non-obvious logic, not the what