🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 7 Min Lesezeit
0

Clean Architecture in a FastAPI + Vue 3 monorepo

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




Why architecture matters in a research project



Most research prototypes are throwaway code. NumPath is not. It needs to survive four phases over 30 weeks, accumulate real student data for a randomised controlled trial, and remain testable without live infrastructure at every step. That means the architecture has to enforce rules that hold up under pressure — not just conventions someone remembers to follow.



This post walks through how NumPath uses Clean Architecture to keep a FastAPI backend, a Vue 3 frontend, and a Python ML module in a single repository without coupling them together.






The monorepo layout



The project lives in a single repo with a clear namespace boundary:




CODE
phd-research/
├── numpath/
│ ├── backend/ # Python 3.12 + FastAPI + SQLAlchemy
│ ├── frontend/ # Vue 3 + Tailwind CSS + Pinia
│ └── ml/ # BKT, DKT, adaptive engine
├── docs/
│ ├── adrs/ # Architecture Decision Records
│ ├── architecture/ # System design, feature specs
│ └── posts/ # This blog series
└── DOMAIN_DICTIONARY.md






The alternative was three separate repos. For a solo PhD project where a data model change touches the migration, the API schema, the ML engine, and the Vue component in the same commit, separate repos mean coordinated PRs across three remotes. That's overhead with no benefit when one person owns all three layers.



The escape hatch is clean: if NumPath ever needs to become a standalone repo, the numpath/ directory lifts out intact.






Dependency direction: the one rule that matters



Clean Architecture has many principles, but only one that I enforce mechanically: inner layers never import from outer layers.



In NumPath's backend, the layers are:




CODE
Domain (models)  →  Use Cases  →  Adapters (routers, DB, LLM)  →  Frameworks (FastAPI, SQLAlchemy)






A use case like GetNextProblemUseCase receives a database session — but it does not import FastAPI, does not know about HTTP, and does not call Depends():




CODE
class GetNextProblemUseCase:
def __init__(self, db: AsyncSession) -> None:
self._db = db

async def execute(self, student_id: uuid.UUID) -> NextProblemResponse:
kc_states = await self._build_kc_states(student_id)
recent_attempts = await self._fetch_recent_attempts(student_id)
recent_mistakes = await self._fetch_recent_mistakes(student_id)

selection: ProblemSelection = select_next_problem(
kc_states=kc_states,
recent_correctness=[row.is_correct for row in recent_attempts],
current_difficulty=recent_attempts[0].difficulty if recent_attempts else 0.3,
recent_mistakes=recent_mistakes,
)

problem = await self._select_problem(selection, ...)
# ... return NextProblemResponse






The router — the adapter layer — is the only file that knows about FastAPI:




CODE
@router.get("/next-problem/{student_id}", response_model=NextProblemResponse)
async def get_next_problem(
student_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
_: dict = Depends(require_student),
) -> NextProblemResponse:
use_case = GetNextProblemUseCase(db)
return await use_case.execute(student_id)






The router does three things: parse the request, inject dependencies, and delegate to the use case. No business logic. If I replaced FastAPI with Litestar tomorrow, I'd rewrite the routers and touch nothing else.






Configuration as a boundary



Settings are another place where framework details leak into domain code if you're not careful. NumPath uses Pydantic's BaseSettings with a .env file:




CODE
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

DATABASE_URL: str = "postgresql+asyncpg://numpath:numpath@localhost:5432/numpath"
LLM_PROVIDER: Literal["claude", "stub"] = "stub"
ANTHROPIC_API_KEY: str = ""
ENVIRONMENT: Literal["development", "production", "test"] = "development"
CORS_ORIGINS: list[str] = ["http://localhost:5173"]






Every secret has a default that works locally. LLM_PROVIDER defaults to "stub" so that tests and local dev never require an API key. The Literal type annotation means a typo in the .env file fails at startup, not at runtime when a teacher clicks "Generate insight."






The ML module as a pure function boundary



The ml/ directory is a separate Python package (numpath-ml) with its own pyproject.toml. The backend depends on it, but the dependency is narrow: two functions and a dataclass.




CODE
from numpath_ml.adaptive_engine import select_next_problem, ProblemSelection
from numpath_ml.bkt import KCState






select_next_problem() takes dictionaries and lists — no SQLAlchemy models, no async, no database. It returns a ProblemSelection with a skill_code, target_difficulty, and reason string. The use case translates between database rows and these pure data structures.



This boundary exists because the ML code changes on a different cadence than the web application. When I replace the rule-based engine with Deep Knowledge Tracing in Phase 2, the use case stays the same — only the function it calls changes.






The frontend: same principle, different language



The Vue 3 frontend mirrors the same layering:



API client — a thin Axios wrapper that handles auth tokens and 401 redirects:




CODE
export const apiClient = axios.create({
baseURL: '/api/v1',
})

apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})






Stores — Pinia stores manage state. The auth store handles login/logout and persists the JWT to localStorage. Views consume stores, not the API client directly.



ViewsPracticeView.vue, TeacherView.vue, LoginView.vue. Each view composes API calls and store access. No view imports another view.



Router — role-based guards redirect students and teachers to their respective views:




CODE
router.beforeEach((to) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) return '/login'
if (to.meta.role && auth.role !== to.meta.role) {
return auth.role === 'teacher' ? '/teacher' : '/practice'
}
})









Docker Compose as the integration layer



The four services — Postgres, Redis, backend, frontend — are composed with health checks so the backend waits for the database:




CODE
backend:
environment:
DATABASE_URL: postgresql+asyncpg://numpath:numpath@postgres:5432/numpath
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./backend:/app/backend
- ./ml:/app/ml
command: uv run uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload






Volume-mounting backend/ and ml/ means hot reload works inside Docker — change a use case, save, and the server restarts. The port mapping (5433:5432 for Postgres) avoids collisions with a local Postgres install.






What this buys you



Three concrete benefits I've already seen:




  1. Test isolation — use cases are testable with a real async database session and no HTTP server. The test creates a GetNextProblemUseCase(db) directly. No FastAPI test client needed for business logic tests.


  2. LLM swappabilityGenerateInsightUseCase receives an LLMProvider protocol. In tests it gets StubProvider. In production it gets ClaudeProvider. The use case doesn't know which one it has.


  3. Safe ML replacement — when BKT gives way to DKT, only numpath_ml changes. The use case calls the same select_next_problem() function with the same signature. The router doesn't change. The frontend doesn't change.







What I'd do differently



I wouldn't change the layering, but I'd add one thing from day one: a fitness function that statically checks import direction. Right now the rule is "use cases don't import routers" — but it's enforced by code review (i.e., me reviewing my own code). A linter rule or CI check that fails on from backend.routers inside use_cases/ would catch drift automatically.






Key Takeaways





  • Dependency direction is the only architectural rule worth enforcing mechanically — inner layers never import outer layers; everything else is convention that erodes under deadline pressure


  • A monorepo is the right default for a solo research project — coordinated PRs across three repos is overhead without benefit when one person owns all layers and changes cut across them


  • Pure function boundaries between modules pay for themselves — the ML module exports two functions and a dataclass; the web layer translates between database rows and those pure structures, making the ML code replaceable without touching the application

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Clean Architecture in a FastAPI + Vue 3 monorepo

Thematisch verwandte Begriffe: Clean, Architecture, FastAPI, monorepo · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...