FastAPI starter kit — strict types, immutable schemas, async SQLAlchemy, 100% coverage gate. Python's answer to nunomaduro/laravel-starter-kit. Phased build, Conventional Commits.
Production-grade FastAPI starter engineered by Socialbug Apps LLC. Built on top of s3rius/FastAPI-template and layered with a services / repositories architecture, ultra-strict tooling, and a fail-fast philosophy.
If nunomaduro/laravel-starter-kit set the bar for "what a senior-grade Laravel starter should look like", SocialpyKit aims for the same bar in Python: every default is opinionated, type-safe, and refuses to ship sloppy code.
- Highlights
- Tech Stack
- Architecture
- Using This Template
- Quick Start
- Development Workflow
- Strict Tooling Principles
- Build Phases
- Configuration
- Testing
- Migrations
- Contributing
- References
- License
- 100% type coverage — every function, method, parameter, return type, and attribute is explicitly typed. No implicit
Any, no baredict/list. - Two type checkers, both strict —
mypy --strictandpyright --strictmust pass with zero errors. - Ruff at maximum strictness —
select = ["ALL"]. The very short ignore list is documented per rule. - Immutable-first DTOs — Pydantic v2
BaseSchemaisfrozen=True, rejectsextrafields, strips whitespace at the boundary. - Services / repositories architecture — routers depend on services, services depend on
BaseRepositoryABCs, never on SQLAlchemy directly. - Async everywhere — async route handlers, async SQLAlchemy 2.0 with
asyncpg, async fixtures,httpx.AsyncClientfor tests. - No lazy loading — eager-load every relationship explicitly (
selectinload/joinedload). - 100% test coverage —
--cov-fail-under=100enforced in CI from Phase 3.4 onward. - Conventional Commits, atomic history — every commit is one logical change, written in the project's documented style.
- No AI attribution — commits never reference Claude / Copilot / "Generated with" footers.
| Layer | Choice |
|---|---|
| Language | Python 3.13+ |
| Framework | FastAPI |
| ORM | SQLAlchemy 2.0 (async, Mapped[T] / mapped_column) |
| Validation | Pydantic v2 (frozen, extra="forbid") |
| Migrations | Alembic |
| Database | PostgreSQL (asyncpg) |
| Auth | JWT (passlib + bcrypt) |
| Observability | Sentry SDK |
| Runtime | Gunicorn + Uvicorn workers |
| Dates | whenever (no datetime.now() / utcnow()) |
| Logging | loguru (no bare print()) |
| Package manager | uv |
| Lint / format | ruff |
| Type checking | mypy + pyright (both strict) |
| Tests | pytest, pytest-asyncio, pytest-cov, httpx |
| Task runner | just |
| Pre-commit | pre-commit |
| CI | GitHub Actions |
Frontend (ui/ in this monorepo) |
Nuxt 3 (hybrid render — SSG marketing, SSR auth, SPA dashboard) + Pinia + $fetch + useCookie + openapi-typescript + shadcn-vue + vee-validate, managed with bun |
app/
api/
v1/
routers/ # one file per domain — call services only
dependencies/ # FastAPI Depends() helpers
core/
config.py # pydantic-settings Settings
security.py # JWT, password hashing
essentials.py # BaseModel, BaseSchema, BaseRepository ABCs
exceptions.py # custom exception hierarchy
logging.py # loguru setup
models/ # SQLAlchemy ORM models (Mapped[T])
schemas/ # Pydantic v2 DTOs (frozen, separate Request / Response)
services/ # business logic, framework-agnostic
repositories/ # data access, SQLAlchemy queries only
db/
session.py # async engine + session factory
base.py # DeclarativeBase
migrations/ # Alembic — never edit manually
tests/
unit/ # services and pure logic
integration/ # DB + API tests via httpx.AsyncClient
conftest.py
ui/ # Nuxt 3 frontend (monorepo, bun-managed)
app.vue # root component
nuxt.config.ts # render rules, modules, runtime config
pages/ # file-based routing
layouts/ # default (marketing) + dashboard (sidebar)
middleware/ # auth.global, auth, admin
plugins/api.ts # $fetch with bearer-token interceptor
composables/useApi.ts
stores/auth.ts # Pinia, useCookie-backed token (SSR-safe)
components/ # .vue components + shadcn-vue under ui/
api/ # openapi-typescript generated types + typed endpoint wrappers
lib/ # utils.ts (cn), api-error.ts
assets/css/main.css
error.vue
package.json
eslint.config.ts
openapi.json # exported from backend, regenerate via `just ui-gen-api`
justfile # all commands (backend, frontend, monorepo)
pyproject.toml # all backend tool config (ruff, mypy, pyright, pytest, coverage)
.pre-commit-config.yaml
CLAUDE.md # AI assistant guardrails (read this before contributing)
AGENTS.md
| From → To | Allowed | Forbidden |
|---|---|---|
| Router → Service | ✅ | — |
| Router → Repository | ❌ | direct repository access |
| Router → Model | ❌ | direct ORM access |
| Service → Repository (interface) | ✅ | — |
| Service → FastAPI primitives | ❌ | services stay framework-agnostic |
| Repository → SQLAlchemy | ✅ | — |
| Repository → business logic | ❌ | only data access |
| Schema → Schema | DTO is frozen=True; never reuse the same schema for input and output |
— |
There are two ways to start a new project from SocialpyKit:
The repo is registered as a GitHub template repository. Click Use this template → Create a new repository on the GitHub UI to clone the source tree verbatim into a fresh repo under your own account.
After cloning, run the rename script with your project name, display name, and GitHub org:
./scripts/rename-project.sh myapi "My API" my-orgThe script (scripts/rename-project.sh) walks the tree with git grep and rewrites every project literal in one pass — env-var prefix, db name, docker image, display name, github org. Review the diff, then commit:
git diff # sanity check
git add -A && git commit -m "chore: rename project from socialpykit"Manual follow-up after the script runs (the script prints these too):
- Update
[project].authorsandmaintainersinpyproject.toml. - Update the copyright holder in
LICENSE. - Set your Sentry DSN env var (e.g.
MYAPI_SENTRY_DSN=...). - Regenerate ui types if you touched the API:
just ui-gen-api. - Recreate the dev database so the new credentials take effect:
docker compose down -v && docker compose up -d db.
copier.yaml declares the variables a future copier render would prompt for (project name, slug, author, sentry toggle, etc.). The source tree is not yet parametrised with Jinja, so copier copy against main is the same as Option A for now. Once the template branch ships, you'll be able to:
uvx copier copy gh:hasankaantan/socialpykit@template ./my-new-serviceUntil then, prefer Option A.
- Python 3.13+ (managed via
.python-version) uv≥ 0.11just≥ 1.51 —brew install justor casey/just releases- Docker + Docker Compose (for the dev database)
git clone git@github.com:hasankaantan/socialpykit.git
cd socialpykit
# --- backend ---
uv sync # install python deps into .venv
uv run pre-commit install # install git hooks
docker compose up -d db # start the dev database
just migrate # run alembic upgrade head
just dev # boot uvicorn with reload
# --- frontend (in a second terminal) ---
just ui-install # install bun deps inside ui/
just ui-dev # boot nuxt dev with hmr- Backend API:
http://localhost:8000. Swagger UI at/api/docs, ReDoc at/api/redoc. - Frontend dev server:
http://localhost:3000(Nuxt default). - Frontend reads the API via
NUXT_PUBLIC_API_BASE(defaults tohttp://localhost:8000). - Hybrid render:
/and/pricingare prerendered to static HTML;/loginand/registerare SSR;/dashboard/**is SPA-only.
Every command lives in the justfile:
# --- backend ---
just dev # uvicorn with reload
just lint # ruff check + ruff format --check
just format # ruff format + ruff check --fix
just types # mypy --strict + pyright --strict
just test-unit # pytest tests/unit
just test-int # pytest tests/integration
just test # lint + types + full pytest run
just migrate # alembic upgrade head
just makemig msg="…" # alembic revision --autogenerate -m "…"
# --- frontend (ui/) ---
just ui-install # bun install inside ui/
just ui-dev # nuxt dev with hmr
just ui-build # nuxt build (hybrid: server + prerendered static)
just ui-generate # nuxt generate (full-static, no server)
just ui-lint # eslint + prettier --check
just ui-format # prettier --write + eslint --fix
just ui-types # nuxt typecheck (vue-tsc against .nuxt/tsconfig.json)
just ui-test # ui-lint + ui-types + ui-build (full frontend pipeline)
just ui-gen-api # regenerate ui/openapi.json + ui/api/schema.ts
# --- monorepo ---
just test-all # just test && just ui-test ← run before every commitRule of thumb: if just test-all is red, the branch is not mergeable.
These are non-negotiable. Every PR is judged against them.
- 100% type coverage. Every function, method, parameter, return, and attribute is typed.
- mypy strict + pyright strict. Both pass with zero errors.
- Ruff
select = ["ALL"]. Ignores are documented per rule. - Immutable-first. Pydantic DTOs are
frozen=True, withextra="forbid". Prefertupleoverlistfor fixed collections. UseFinal[T]for module-level constants. - Fail-fast. Validate at boundaries via Pydantic. Never pass raw
dictacross layers. Raise early. Never swallow exceptions silently. - 100% test coverage. From Phase 3.4 onward,
pytest --cov-fail-under=100. - DRY + SOLID. Shared logic in services / core utilities. Depend on abstractions (
BaseRepository), not concretions. - Async by default. All route handlers and repository methods are
async def. wheneverfor dates. Neverdatetime.now()ordatetime.utcnow().logurufor logs. Neverprint().Depends()for DI. Never instantiate services or repositories manually inside routers.pydantic-settingsfor config. No scatteredos.environ.get().- Alembic for every schema change. Never
Base.metadata.create_all()in production code paths. - Eager loading explicitly.
selectinload/joinedloadfor every relationship — never lazy.
SocialpyKit is built in deliberate phases. Each step within a phase is its own commit (Conventional Commits, present tense, no AI attribution).
| Phase | Status | Scope |
|---|---|---|
| Phase 0 — Base template | ✅ Done | fastapi_template scaffold, cleanup, Python 3.13 pin |
| Phase 1 — Tooling | ✅ Done | Strict pyproject.toml, justfile, pre-commit, pipeline verification |
| Phase 2 — Architecture refactor | ✅ Done | essentials.py, exception hierarchy, services / repositories, whenever migration |
| Phase 3 — Test & coverage | ✅ Done | Shared fixtures, unit + integration suites, 100% coverage gate enforced |
| Phase 4 — AI / dev experience | ✅ Done | CLAUDE.md, AGENTS.md, .mcp.json, .cursor/ rules |
Phase 5 — Vue 3 frontend (monorepo ui/) |
✅ Done | Vite + Pinia + Axios + openapi-typescript, bun-managed, ESLint strict (superseded by Phase 8) |
| Phase 6 — Template parameterization | ✅ Done (soft) | copier.yaml variables, repo marked as GitHub template, scripts/rename-project.sh automates the rename |
| Phase 7 — Auth + dashboard | ✅ Done | JWT register/login/me, user RBAC with admin role, self profile update + delete, admin user CRUD, shadcn-vue dashboard with sidebar, vee-validate forms, vue-router auth/admin guards |
| Phase 8 — Nuxt 3 hybrid migration | ✅ Done | Replaces the Vite SPA with Nuxt 3: SSG for marketing (/, /pricing), SSR for auth (/login, /register), SPA for /dashboard/**. useCookie token, $fetch over axios, useApi() composable, file-based routing, global+named middleware, @nuxtjs/sitemap + @nuxtjs/robots. |
For the detailed phase-by-phase commit plan, see CLAUDE.md.
Configuration is driven by environment variables, parsed via pydantic-settings. All variables use the SOCIALPYKIT_ prefix.
Example .env (do not commit this — it is .gitignored):
SOCIALPYKIT_RELOAD=True
SOCIALPYKIT_HOST=0.0.0.0
SOCIALPYKIT_PORT=8000
SOCIALPYKIT_ENVIRONMENT=dev
SOCIALPYKIT_DB_HOST=localhost
SOCIALPYKIT_DB_PORT=5432
SOCIALPYKIT_DB_USER=socialpykit
SOCIALPYKIT_DB_PASS=socialpykit
SOCIALPYKIT_DB_BASE=socialpykit
SOCIALPYKIT_SENTRY_DSN=
# JWT — see warning below
SOCIALPYKIT_JWT_SECRET_KEY=replace-this-in-production
SOCIALPYKIT_JWT_ALGORITHM=HS256
SOCIALPYKIT_JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30Every Settings field is reflected as SOCIALPYKIT_<UPPERCASE_FIELD>. See app/settings.py for the full surface.
⚠️ SOCIALPYKIT_JWT_SECRET_KEYis required in production. The default value inapp/settings.pyis the literal stringdev-only-do-not-use-in-productionand it ships intentionally weak so a forgotten override is immediately obvious. Generate a strong key once per environment:python -c "import secrets; print(secrets.token_urlsafe(64))"Set it via your deployment platform's secret manager — never commit it.
The default dev setup serves plaintext HTTP between ui and api — fine for localhost, never for production. In production you must terminate TLS at a reverse proxy and keep the FastAPI app on an internal network. JWT bearer tokens travelling over plaintext can be sniffed; TLS 1.3 is the baseline.
A ready-to-use Caddy + Docker Compose stack lives under deploy/. Caddy handles Let's Encrypt issuance and renewal automatically, adds HSTS + security headers, and proxies the API while serving the built frontend statically.
| Pattern | When to pick it |
|---|---|
| Reverse proxy on the same host (Caddy / Nginx / Traefik) | Single-server deployments. This is what deploy/ ships. |
| Cloud load balancer / CDN (Cloudflare, AWS ALB, GCP LB) | Multi-instance setups; certificate lives at the LB, backend stays plaintext on a private network. |
Uvicorn --ssl-keyfile/--ssl-certfile |
Niche; only when no proxy is allowed. Rare in 2026. |
Frontend and API typically live on separate subdomains (app.example.com / api.example.com). Two settings tie them together:
# frontend — runtime config, read by Nuxt at request time (also overridable
# at build time via the same env var, since Nuxt picks it up from
# nuxt.config.ts → runtimeConfig.public.apiBase).
NUXT_PUBLIC_API_BASE=https://api.example.com
# frontend — sitemap / robots / SEO need the canonical site URL.
NUXT_PUBLIC_SITE_URL=https://app.example.com
# backend — JSON list, parsed by pydantic-settings
SOCIALPYKIT_CORS_ORIGINS=["https://app.example.com"]See deploy/README.md for the full step-by-step (DNS, .env.prod, frontend build, docker compose up, admin bootstrap, hardening checklist).
No. TLS 1.3 already uses AES-256-GCM (or ChaCha20-Poly1305). Adding a second AES layer in the API client doesn't add a real trust boundary — the key would have to ship with the client. Stick to TLS + HSTS + short-lived JWTs. The only legitimate "extra encryption" pattern for a 2026 SaaS API is end-to-end encryption where the server is explicitly untrusted (password managers, Signal-style messaging) — outside this template's scope.
Tests run against a real PostgreSQL instance. We do not mock the database in integration tests — production parity matters more than test startup time.
# start the test database (or reuse the dev one)
docker compose up -d db
# fast loop
just test
# layered targets (after Phase 3)
just test-unit
just test-intIntegration tests use httpx.AsyncClient + ASGITransport. The session-scoped _engine fixture creates and drops a dedicated socialpykit_test database per test session; each test gets its own dbsession with a SAVEPOINT-style rollback.
just migrate # apply all pending migrations
just makemig msg="add users" # autogenerate a new revisionUnder the hood:
uv run alembic upgrade head
uv run alembic revision --autogenerate -m "…"
uv run alembic downgrade <revision_id>
uv run alembic downgrade baseNever edit migrations under app/db/migrations/versions/ by hand. Always regenerate with --autogenerate.
Contributions are welcome — and held to the same bar as internal work.
- Read
CLAUDE.mdfirst. It documents every guardrail. - Branch from
main. Name itfeat/<thing>,fix/<thing>,chore/<thing>, etc. - One logical change per commit. Use Conventional Commits:
feat:new featurefix:bug fixchore:build / toolingrefactor:no behaviour changetest:tests onlydocs:documentationci:GitHub Actions
- Present tense, lowercase, ≤ 72 chars on the first line. Body wraps at 72.
- No AI attribution. Do not add
Co-Authored-By: Claudeor "Generated with X" footers. just testmust pass. Lint, both type checkers, and pytest — all green before opening a PR.- Touch only what the change requires. Bug fixes are not refactor opportunities. If you spot adjacent dead code, mention it in the PR description; do not silently rewrite it.
- No
# type: ignorewithout a specific rule code and explanation. Same for# noqa. - No
print(). Useloguru.logger. - No
datetime.now()/datetime.utcnow(). Usewhenever.
PRs that ignore these rules will be sent back. PRs that follow them tend to merge fast.
| Repo | Why it's referenced |
|---|---|
| nunomaduro/laravel-starter-kit | Inspiration — every strictness principle is adapted from here |
| nunomaduro/laravel-starter-kit-inertia-vue | Frontend architecture reference |
| s3rius/FastAPI-template | Base template the project is generated from |
| fastapi/full-stack-fastapi-template | Official FastAPI full-stack reference |
| zhanymkanov/fastapi-best-practices | FastAPI best-practices guide |
| nunomaduro/essentials | Laravel Essentials — Python equivalent lives in app/core/essentials.py |
MIT — see LICENSE. Copyright © 2026 Socialbug Apps LLC.
Socialbug Apps LLC — Hasan Kaan Tan (@hasankaantan)
Stack lead: Laravel → FastAPI migration. Standards: PHPStan-equivalent strictness, Pest-equivalent coverage, SOLID, DRY, Conventional Commits.