docker-compose up -d --builduv run pytest
uv run pytest -v
uv run pytest --cov=app
uv run pytest app/tests/test_users.pyuv run ruff format .
uv run ruff check --fix .uv run alembic upgrade head
uv run alembic revision --autogenerate -m "description"
uv run alembic downgrade -1
uv run alembic historyuv add <package>
uv add --dev <package>
uv sync- Copy
.env.example→.env - Python >= 3.12 required (see
.python-version) - Use
uvonly — neverpip - Start DB first:
docker-compose up -d dbAPI: http://localhost:8000
Swagger: http://localhost:8000/docs
| Variable | Default | Description |
|---|---|---|
PROJECT_NAME |
FastAPI Template |
Project display name |
SECRET_KEY |
— | Long random string, never hardcode |
ENVIRONMENT |
local |
local, development, or production |
POSTGRES_SERVER |
localhost |
PostgreSQL host |
POSTGRES_USER |
postgres |
PostgreSQL user |
POSTGRES_PASSWORD |
— | PostgreSQL password |
POSTGRES_DB |
app |
PostgreSQL database name |
FIRST_SUPERUSER |
admin@example.com |
Initial admin email |
FIRST_SUPERUSER_PASSWORD |
— | Initial admin password |
CLOUDINARY_CLOUD_NAME |
— | Cloudinary cloud name (required for POST /upload) |
CLOUDINARY_API_KEY |
— | Cloudinary API key (required for POST /upload) |
CLOUDINARY_API_SECRET |
— | Cloudinary API secret (required for POST /upload) |
CLOUDINARY_UPLOAD_FOLDER |
uploads |
Cloudinary folder uploads are stored under |
Never commit
.env. It is already in.gitignore.
Strict layered structure — dependency flow is one-directional:
api → services → repositories → models
api/routes/→ Routing only. UsesDepends()for session and current user. No business logic.api/dependencies/→ Shared FastAPI dependencies (get_session,get_current_user, etc.).services/→ All business logic. Receives session as argument. May raiseHTTPExceptiondirectly.repositories/→ All DB queries. No business rules. Receives session as argument.models/→ SQLAlchemy ORM definitions only. No logic.schemas/→ Pydantic DTOs at the API boundary (request/response). SeparateCreate,Update,Responseper domain. Data that never crosses the API (internal value objects passed between layers) uses a plain@dataclass, not a Pydantic schema — e.g.core/broadcast_templates.py:RenderedContent,services/admin/broadcast_email.py:BroadcastEmail.core/→ Config, JWT auth, bcrypt, exceptions, logging.utils/→ Pure helper functions shared across layers.
These are non-negotiable:
- No classes for repositories or services — use pure async functions
Depends()is used only in route handlers, never in service or repository functionsapi/routes/never calls repositories directly — always goes throughservices/- Services never call other services — use shared repository functions or
use_cases/instead - No sync DB calls — all queries must be
async/await - No raw ORM objects returned from endpoints — always map to Pydantic schemas
schemas/(Pydantic) is for API-boundary DTOs only; internal value objects passed between layers use a plain@dataclass, never a Pydantic schema- No secrets in code — always use
pydantic-settingsand.env - Error messages must ALWAYS come from
app/core/messages/error_message.py - Success messages must ALWAYS come from
app/core/messages/success_message.py - Never hardcode error or success strings inline (e.g.,
detail="User not found") - Always create an Alembic migration after changing a model
- Always commit
uv.locktogether with dependency changes - Never use
pip install— useuv addonly - All functions must have a docstring — minimum one line, always in English
HTTPExceptioncan be raised in both services and route handlers- Use guard clauses at the top of functions — fail fast, happy path last
- Register global handlers in
app/main.pyfor unexpected errors - Never expose internal errors or stack traces to clients
- All services must log critical failures
- Use structured logging (JSON format in production)
- Error messages must ALWAYS be retrieved from
app/core/messages/error_message.py - Success messages must ALWAYS be retrieved from
app/core/messages/success_message.py - Never hardcode error or success strings (e.g., do not write
detail="User not found")
- All JWT logic lives in
app/core/security.py - Protect routes with
Depends(get_current_user)defined inapp/api/dependencies/ - Always hash passwords with
bcrypt— never store or log plain-text passwords - Rate limiting is handled via
slowapi— configure inapp/core/ - First superuser is seeded automatically on startup using
FIRST_SUPERUSERandFIRST_SUPERUSER_PASSWORD
main→ productiondevelop→ stagingfeature/*→ new featuresfix/*→ bugfix
Use conventional commits:
feat: add user activity log
fix: prevent duplicate email registration
refactor: optimize repository queries
chore: update dependencies
- Follow the layered architecture strictly — never bypass the service layer
- Never generate classes for services or repositories — use pure async functions
- Never place
Depends()inside service or repository functions - Never generate sync DB code — always
async/await - Never hardcode error/success message strings — always import from
core/messages/ - Always write a docstring for every function — minimum one line, in English
- Never ignore Hard Rules above
- Prefer small, incremental changes over large rewrites
- Always include tests when adding new logic
- When adding a new domain, create all layers: model → schema → repository → service → route
- Always generate an Alembic migration when touching
models/
GitHub Actions runs on every push:
uv run ruff check .uv run pytest- Docker build check
All checks must pass before merge.