Architecture¶
shaapi follows a clean, layered architecture inspired by fastapi-best-architecture, trimmed to a lean core.
Project layout¶
my_api/
├── docker-compose.yml # base stack (api, postgres, redis, minio)
├── docker-compose.override.yml # dev: source bind-mount + hot-reload
├── docker-compose.monitoring.yml # opt-in observability
├── docker-run.sh # orchestration wrapper
├── Dockerfile # multi-stage slim image (built with uv)
├── pyproject.toml # dependencies (uv)
├── .env / .env.template # configuration
└── backend/
├── main.py # app entry point (parent app + lifespan)
├── core/
│ ├── conf.py # settings (pydantic-settings)
│ ├── registrar.py # app factory: middleware, routers, lifespan
│ └── path_conf.py
├── app/admin/ # the "admin" sub-application
│ ├── api/v1/ # routers (auto-discovered)
│ ├── schema/ # Pydantic schemas
│ └── service/ # business logic
├── models/ # SQLAlchemy models
├── crud/ # data access (CRUDBase / sqlalchemy-crud-plus)
├── common/ # security, responses, exceptions, storage…
├── middleware/ # JWT, i18n, opera-log, state, trace…
├── database/ # Postgres + Redis clients
└── alembic/ # migrations
The layers¶
A request flows through clear layers, each with one responsibility:
HTTP → router (api/v1) → service → crud → model (DB)
│ │ │
schema business data
validation rules access
- models/ — SQLAlchemy 2 declarative models (the database tables).
- schema/ — Pydantic v2 models for request/response validation.
- crud/ — thin data-access classes extending
CRUDBase(sqlalchemy-crud-plus):select_model,create_model,update_model… - service/ — business logic, transactions (
async_db_session.begin()), authorization checks. - api/v1/ — FastAPI routers returning a unified
ResponseModel.
Router auto-discovery¶
Any *.py in app/admin/api/v1/ that defines a router is loaded
automatically (backend/app/__init__.py). Add a file, get endpoints — no
central registry to edit.
The app & its lifespan¶
backend/main.py builds a thin parent FastAPI app that mounts the feature
sub-apps (currently /admin). The parent owns the lifespan (creating DB
tables in dev, opening Redis, initializing the rate limiter) because Starlette
does not run the lifespan of mounted sub-applications.
Configuration¶
core/conf.py defines all settings with safe development defaults, so the app
boots with an empty .env. Override via environment variables / .env. Key
flags:
| Setting | Default | Meaning |
|---|---|---|
ENVIRONMENT |
dev |
dev enables reload; prod serves baked code |
DB_AUTO_CREATE |
true |
Auto-create tables on startup (dev). Off in prod. |
OBSERVABILITY_ENABLED |
false |
Prometheus/OpenTelemetry (opt-in) |
Database & migrations¶
- Dev: tables are auto-created on startup (
DB_AUTO_CREATE=true) for a zero-friction first run. - Prod: set
DB_AUTO_CREATE=falseand use Alembic migrations (alembic upgrade head, run automatically by the entrypoint). Author new migrations withshaapi db generate --message "message".
Migrations are the source of truth and are committed to your repo.
Built in¶
- Auth: JWT (access + refresh), register/login,
DependsJwtAuth. - RBAC: Casbin policies; users, roles, permissions.
- Storage: MinIO / S3 / GCS via a common interface.
- Observability (opt-in): Prometheus metrics, OpenTelemetry traces, Grafana + Tempo + Loki.
- Cross-cutting: request IDs, operation logs, login logs, i18n, rate limiting, unified responses and error handling.
Tech stack¶
FastAPI · SQLAlchemy 2 (async) · Pydantic v2 · Alembic · PostgreSQL · Redis · Casbin · MinIO · uv · Docker. See Create a feature to build on top of it.