Skip to content

Commit 4c3e16d

Browse files
committed
refactor: refactor project structure
first refactor project structure, add CLAUDE.md and AGENTS.md Changelog: refactor
1 parent fbaf2db commit 4c3e16d

53 files changed

Lines changed: 952 additions & 687 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:github.com)",
5+
"WebFetch(domain:raw.githubusercontent.com)",
6+
"Bash(uv run:*)"
7+
]
8+
}
9+
}

.env renamed to .env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Domain
22
# This would be set to the production domain with an env var on deployment
3-
# used by Traefik to transmit traffic and aqcuire TLS certificates
3+
# used by Traefik to transmit traffic and acquire TLS certificates
44
DOMAIN=localhost
55
# To test the local Traefik config
66
# DOMAIN=localhost.tiangolo.com
@@ -18,6 +18,8 @@ STACK_NAME=full-stack-fastapi-project
1818

1919
# Backend
2020
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
21+
22+
# python -c "import secrets; print(secrets.token_urlsafe(32))"
2123
SECRET_KEY=changethis
2224
FIRST_SUPERUSER=admin@example.com
2325
FIRST_SUPERUSER_PASSWORD=changethis

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ node_modules/
55
/playwright-report/
66
/blob-report/
77
/playwright/.cache/
8+
9+
# Environment files (use .env.example as template)
10+
.env
11+
.env.local

AGENTS.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Full-stack web application template: **FastAPI** backend + **React/TypeScript** frontend. Monorepo with `backend/` and `frontend/` as separate workspaces managed by **uv** (Python) and **bun** (JS).
8+
9+
## Common Commands
10+
11+
### Running the Stack (Docker Compose — recommended)
12+
```bash
13+
docker compose watch # Start all services with hot-reload
14+
docker compose up -d # Start in background
15+
docker compose logs backend -f # Tail backend logs
16+
```
17+
18+
### Backend
19+
```bash
20+
cd backend
21+
uv sync # Install dependencies
22+
fastapi dev app/main.py # Dev server on :8000 (hot-reload)
23+
24+
# Lint & type-check
25+
bash scripts/lint.sh # Runs: mypy app && ruff check app && ruff format app --check
26+
27+
# Tests (requires running PostgreSQL — use docker compose for DB)
28+
bash scripts/test.sh # coverage run + pytest + report
29+
uv run pytest tests/ # Run all tests
30+
uv run pytest tests/api/routes/test_items.py # Single test file
31+
uv run pytest tests/api/routes/test_items.py::test_create_item # Single test
32+
33+
# Alembic migrations
34+
uv run alembic revision --autogenerate -m "description"
35+
uv run alembic upgrade head
36+
```
37+
38+
### Frontend
39+
```bash
40+
cd frontend
41+
bun install # Install dependencies
42+
bun run dev # Dev server on :5173
43+
bun run build # TypeScript check + Vite build
44+
bun run lint # Biome check --write --unsafe
45+
bun run test # Playwright E2E tests
46+
```
47+
48+
### Regenerate Frontend API Client
49+
```bash
50+
# Requires backend running (exports OpenAPI JSON, generates TypeScript client)
51+
bash scripts/generate-client.sh
52+
```
53+
54+
### Pre-commit Hooks
55+
```bash
56+
cd backend && uv run prek install -f # Install hooks (uses prek, not pre-commit)
57+
uv run prek run --all-files # Run all hooks manually
58+
```
59+
60+
## Architecture
61+
62+
### Backend (`backend/`)
63+
- **FastAPI** with **SQLModel** (SQLAlchemy + Pydantic hybrid) ORM
64+
- **PostgreSQL 18** via `psycopg` (psycopg3) driver
65+
- **Alembic** for DB migrations (auto-runs on container start via `prestart` service)
66+
- **JWT auth** (HS256 via `pyjwt`) with **Argon2** password hashing (bcrypt legacy fallback via `pwdlib`)
67+
- API routes: `backend/app/api/routes/``login`, `users`, `items`, `utils`, `private` (local-only)
68+
- Settings via `pydantic-settings` reading from root `.env` file, split into sub-configs (`DatabaseSettings`, `EmailSettings`)
69+
- Email via `emails` library + Jinja2 templates (MJML source → compiled HTML in `email-templates/`)
70+
- Sentry integration (disabled in `local` environment)
71+
- API docs hidden in production (only available in `local`/`staging`)
72+
73+
#### Domain-based structure
74+
Code is organized by domain, each with its own models, service, dependencies, and constants:
75+
76+
```
77+
backend/app/
78+
├── main.py # App entry point, CORS, Sentry, global DomainError handler
79+
├── api/
80+
│ ├── main.py # Router mounting
81+
│ ├── deps.py # Common dependencies (SessionDep, CurrentUser, etc.)
82+
│ └── routes/ # Thin route handlers (HTTP protocol only)
83+
│ ├── login.py
84+
│ ├── users.py
85+
│ ├── items.py
86+
│ ├── utils.py
87+
│ └── private.py # Local-only, uses service layer
88+
├── core/
89+
│ ├── config.py # Settings + DatabaseSettings + EmailSettings
90+
│ ├── security.py # JWT token creation, password hashing
91+
│ └── db.py # Engine, naming convention, init_db
92+
├── common/
93+
│ ├── models.py # Shared models (Message, Token, TokenPayload, NewPassword), get_datetime_utc
94+
│ └── exceptions.py # DomainError base class, InvalidResetTokenError, CredentialsValidationError
95+
├── users/
96+
│ ├── models.py # User, UserCreate, UserUpdate, UserPublic, etc.
97+
│ ├── service.py # Business logic (create, update, authenticate, etc.)
98+
│ ├── dependencies.py # valid_user_id, ValidUser
99+
│ ├── exceptions.py # UserNotFoundError, UserAlreadyExistsError, etc.
100+
│ └── constants.py # Success message constants (PASSWORD_UPDATED, USER_DELETED)
101+
├── items/
102+
│ ├── models.py # Item, ItemCreate, ItemUpdate, ItemPublic, etc.
103+
│ ├── service.py # Business logic (create, update, delete, get_items)
104+
│ ├── dependencies.py # valid_item_id, ValidItem, valid_owned_item (chained)
105+
│ ├── exceptions.py # ItemNotFoundError, ItemPermissionError
106+
│ └── constants.py # Success message constants (ITEM_DELETED)
107+
├── utils.py # Email utilities (send, generate templates)
108+
├── initial_data.py # Seed superuser
109+
└── backend_pre_start.py # DB readiness check
110+
```
111+
112+
Key conventions:
113+
- **Routes are thin** — only HTTP protocol handling, business logic in `service.py`
114+
- **Dependencies chain** — e.g. `valid_owned_item` depends on `valid_item_id` (FastAPI caches per request)
115+
- **Domain exceptions** — each domain defines exception classes inheriting `DomainError(status_code, detail)`; a single global handler in `main.py` converts any `DomainError` to the matching HTTP response. No `HTTPException` in the codebase.
116+
- **Constants are for success messages only** — error messages live in exception classes as the single source of truth
117+
- **Settings sub-configs** — access via `settings.db.POSTGRES_*`, `settings.email.SMTP_*`
118+
119+
Key files:
120+
- `app/core/config.py``Settings`, `DatabaseSettings`, `EmailSettings`
121+
- `app/core/db.py` — Engine + DB naming convention (`pk_`, `fk_`, `ix_`, `uq_`, `ck_`)
122+
- `app/api/deps.py` — Dependency injection (`SessionDep`, `CurrentUser`)
123+
- `app/common/exceptions.py``DomainError` base class (all domain exceptions inherit from it, carrying `status_code` + `detail`)
124+
125+
### Frontend (`frontend/`)
126+
- **React 19** + **TypeScript 5.9** + **Vite 7** (SWC plugin)
127+
- **TanStack Router** (file-based routing in `src/routes/`, auto-generates `routeTree.gen.ts`)
128+
- **TanStack Query** (React Query v5) for server state
129+
- **Tailwind CSS v4** + **shadcn/ui** components (in `src/components/ui/`)
130+
- **Biome v2** for linting/formatting
131+
- API client auto-generated from OpenAPI spec via `@hey-api/openapi-ts``src/client/`
132+
- Forms: `react-hook-form` + `zod v4` validation
133+
- Auth guard in `_layout.tsx` redirects unauthenticated users to `/login`
134+
135+
### Auto-generated Files (do not edit manually)
136+
- `frontend/src/client/` — Generated API client (regenerate via `scripts/generate-client.sh`)
137+
- `frontend/src/routeTree.gen.ts` — Generated by TanStack Router plugin
138+
- `backend/app/email-templates/build/` — Compiled from MJML sources
139+
140+
### Docker Services
141+
- `db` — PostgreSQL 18
142+
- `prestart` — Runs migrations + seeds superuser
143+
- `backend` — FastAPI (port 8000)
144+
- `frontend` — Nginx serving built React app (port 80→5173 in dev)
145+
- `mailcatcher` — Local email testing (SMTP 1025, Web UI 1080)
146+
- Traefik reverse proxy routes: `api.<DOMAIN>` → backend, `dashboard.<DOMAIN>` → frontend
147+
148+
## Code Style & Conventions
149+
150+
### Backend
151+
- Python ≥3.10, mypy strict mode
152+
- Ruff rules: pycodestyle, pyflakes, isort, flake8-bugbear, comprehensions, pyupgrade, no print statements
153+
- Alembic directory excluded from ruff and mypy
154+
- Alembic migration naming: `YYYY-MM-DD_slug_revid.py` (via `file_template` in `alembic.ini`)
155+
- DB index naming convention: `pk_<table>`, `fk_<table>_<col>_<ref_table>`, `ix_<col>`, `uq_<table>_<col>`, `ck_<table>_<name>`
156+
- Tests require a running PostgreSQL instance; CI enforces ≥90% coverage
157+
158+
### Frontend
159+
- Biome excludes: `dist/`, `src/routeTree.gen.ts`, `src/client/`, `src/components/ui/`
160+
- Path alias: `@` maps to `src/`
161+
- Dark mode default via `next-themes`
162+
163+
## Environment
164+
- **`.env.example`** — Committed template with safe placeholder values (`changethis`). Developers copy to `.env` locally.
165+
- **`.env`** — Local config (gitignored). Shared by backend (pydantic-settings) and Docker Compose.
166+
- **`.env.local`** — Optional personal overrides (gitignored). Values here override `.env`, just like frontend's `.env.local` pattern.
167+
- **Priority chain**: environment variables > `.env.local` > `.env` > code defaults
168+
- Settings via `pydantic-settings` with `env_file=("../.env", "../.env.local")` — later files override earlier ones
169+
- Sub-configs: `settings.db.POSTGRES_*`, `settings.email.SMTP_*`
170+
- Key variables: `DOMAIN`, `SECRET_KEY`, `FIRST_SUPERUSER`/`FIRST_SUPERUSER_PASSWORD`, `POSTGRES_*`, `SMTP_*`
171+
- Production/CI: inject via environment variables (highest priority), no env files needed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

backend/alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
script_location = app/alembic
66

77
# template used to generate migration files
8-
# file_template = %%(rev)s_%%(slug)s
8+
file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s_%%(rev)s
99

1010
# timezone to use when rendering the date
1111
# within the migration file as well as the filename.

backend/app/alembic/env.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515

1616
# add your model's MetaData object here
1717
# for 'autogenerate' support
18-
# from myapp import mymodel
19-
# target_metadata = mymodel.Base.metadata
20-
# target_metadata = None
2118

22-
from app.models import SQLModel # noqa
23-
from app.core.config import settings # noqa
19+
from app.users.models import SQLModel # noqa: E402
20+
from app.items.models import Item # noqa: E402, F401
21+
from app.core.config import settings # noqa: E402
2422

2523
target_metadata = SQLModel.metadata
2624

@@ -31,7 +29,7 @@
3129

3230

3331
def get_url():
34-
return str(settings.SQLALCHEMY_DATABASE_URI)
32+
return str(settings.db.SQLALCHEMY_DATABASE_URI)
3533

3634

3735
def run_migrations_offline():

backend/app/alembic/versions/1a31ce608336_add_cascade_delete_relationships.py

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Initialize models
2+
3+
Revision ID: 0001
4+
Revises:
5+
Create Date: 2026-03-14
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
from alembic import op
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "0001"
15+
down_revision = None
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade() -> None:
21+
op.create_table(
22+
"user",
23+
sa.Column("email", sa.String(length=255), nullable=False),
24+
sa.Column("is_active", sa.Boolean(), nullable=False),
25+
sa.Column("is_superuser", sa.Boolean(), nullable=False),
26+
sa.Column("full_name", sa.String(length=255), nullable=True),
27+
sa.Column("id", sa.Uuid(), nullable=False),
28+
sa.Column(
29+
"hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
30+
),
31+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
32+
sa.PrimaryKeyConstraint("id", name="pk_user"),
33+
)
34+
op.create_index("ix_user_email", "user", ["email"], unique=True)
35+
op.create_table(
36+
"item",
37+
sa.Column("title", sa.String(length=255), nullable=False),
38+
sa.Column("description", sa.String(length=255), nullable=True),
39+
sa.Column("id", sa.Uuid(), nullable=False),
40+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
41+
sa.Column("owner_id", sa.Uuid(), nullable=False),
42+
sa.ForeignKeyConstraint(
43+
["owner_id"],
44+
["user.id"],
45+
name="fk_item_owner_id_user",
46+
ondelete="CASCADE",
47+
),
48+
sa.PrimaryKeyConstraint("id", name="pk_item"),
49+
)
50+
51+
52+
def downgrade() -> None:
53+
op.drop_table("item")
54+
op.drop_index("ix_user_email", table_name="user")
55+
op.drop_table("user")

0 commit comments

Comments
 (0)