5.9 KiB
5.9 KiB
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.examplewith placeholder values and comments explaining each variable - Load settings via
pydantic-settings:
# 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/
# 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
Mappedsyntax:
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
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.pywith a consistent prefix and tags
Docker Compose
- Use named volumes for persistent data (database, uploads)
- Always define a
healthcheckfor the database service - Use
depends_onwithcondition: service_healthyfor the backend - Keep the Nginx config in a
nginx/folder and mount it as a volume
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
Ntfy Notifications
- All web apps must integrate Ntfy push notifications for admin-relevant events
- Use a self-hosted Ntfy instance; URL and topic stored in
.env— never hardcoded - Send notifications via HTTP POST using
httpx(async) — do not add a separate Ntfy library
# app/utils/notify.py
import httpx
from app.config import settings
async def notify(title: str, message: str, priority: str = "default", tags: list[str] | None = None):
headers = {
"Title": title,
"Priority": priority,
}
if tags:
headers["Tags"] = ",".join(tags)
async with httpx.AsyncClient() as client:
await client.post(
f"{settings.ntfy_url}/{settings.ntfy_topic}",
content=message,
headers=headers,
)
Required .env variables:
NTFY_URL=https://ntfy.example.com
NTFY_TOPIC=your-topic
Standard events to notify on (adapt to what the app actually does):
| Event | Priority | Tags |
|---|---|---|
| Successful admin login | high | warning |
| Failed admin login (threshold reached) | urgent | rotating_light |
| New user registration | default | busts_in_silhouette |
| User account deletion | default | wastebasket |
| Role/permission escalation | high | shield |
| Password reset requested | default | key |
| Rate limit triggered | default | no_entry |
| API key created or revoked | high | key |
| Service startup / crash recovery | default | white_check_mark / x |
| High error rate (5xx spike) | high | rotating_light |
| Large data export initiated | default | outbox_tray |
Rules:
- Add Ntfy to every new web app from the start — it is checked during go-live review
- Only notify on events that are genuinely useful to an admin — don't spam low-value noise
- Failed notifications must not crash the app — wrap calls in
try/exceptand log the error
Testing
- Do not create test files — I'll run the code directly to verify it works
- If asked to write tests, use pytest with pytest-asyncio; use a separate test DB, never the real one
Comments
- Docstrings on all functions that aren't self-explanatory
- Comment the why for non-obvious logic, not the what