chartbastan/docs/architecture-patterns.md
2026-02-01 09:31:38 +01:00

758 lines
16 KiB
Markdown

# Patterns d'Architecture - ChartBastan
## Vue d'ensemble
ChartBastan utilise une architecture **client-serveur** avec:
- **Frontend:** Application React/Next.js (SPA avec SSR)
- **Backend:** API REST FastAPI
- **Communication:** REST API via HTTP
- **Architecture asynchrone:** RabbitMQ pour tâches lourdes
---
## Partie 1: Frontend Architecture (chartbastan/)
### Type d'Architecture: Component-Based avec App Router
**Framework:** Next.js 16 App Router
**Caractéristiques Principales:**
#### 1. Server Components vs Client Components
**Server Components (par défaut):**
- Exécutés sur le serveur
- Rendu HTML côté serveur
- Accès direct aux ressources serveur
- Pas d'hydratation nécessaire
- Utilisés pour: layouts, pages, data fetching initial
**Client Components (directive 'use client'):**
- Exécutés sur le client
- Hydratation côté client
- Interactivité utilisateur (events, state)
- Utilisés pour: formulaires, interactions, state local
**Exemple:**
```typescript
// Server Component (par défaut)
export default async function MatchPage() {
const matches = await fetchMatches(); // Exécuté sur serveur
return <MatchList matches={matches} />;
}
// Client Component
"use client";
export function MatchList({ matches }: { matches: Match[] }) {
const [selected, setSelected] = useState(null); // State local
return <div onClick={() => setSelected(matches[0])} />;
}
```
#### 2. App Router Structure
**Routing:** Système de fichiers dans `src/app/`
**Conventions:**
- `page.tsx` - Page principale de la route
- `layout.tsx` - Layout partagé pour la route et ses enfants
- `loading.tsx` - UI de chargement (automatic streaming)
- `error.tsx` - UI d'erreur (error boundary)
- `not-found.tsx` - UI pour 404
- `route.ts` - API routes (backend intégré)
**Exemple de structure:**
```
src/app/
├── layout.tsx # Layout racine
├── page.tsx # Page d'accueil (/)
├── login/
│ └── page.tsx # Page de login (/login)
├── matches/
│ ├── page.tsx # Liste des matchs (/matches)
│ ├── [id]/
│ │ └── page.tsx # Détail d'un match (/matches/123)
│ └── loading.tsx # Loading pour /matches
└── api/
└── auth/
└── route.ts # API route POST /api/auth
```
#### 3. Data Fetching Strategy
**Approche Hybride:**
**Server-Side Rendering (SSR):**
- Data fetch dans Server Components
- Cache avec React Cache
- Revalidation avec revalidatePath
- Avantages: SEO, premier rendu rapide
**Client-Side Fetching:**
- React Query pour data interactive
- Cache local et synchronisation
- Refetch automatique
- Optimistic updates
**Exemple:**
```typescript
// Server Component: SSR
import { fetchMatches } from '@/lib/data';
export default async function MatchesPage() {
const matches = await fetchMatches(); // Cache automatique
return <MatchList initialData={matches} />;
}
// Client Component: React Query
"use client";
import { useQuery } from '@tanstack/react-query';
export function MatchList({ initialData }) {
const { data } = useQuery({
queryKey: ['matches'],
queryFn: fetchMatches,
initialData, // Hydratation depuis SSR
refetchInterval: 30000, // Refetch toutes les 30s
});
}
```
#### 4. State Management
**Zustand (Global State):**
- Stores légers et simples
- Pas de Provider/Context nécessaires
- Sélecteurs pour optimisation
- DevTools intégrés
**Exemple:**
```typescript
// store/user.ts
import { create } from 'zustand';
interface UserStore {
user: User | null;
setUser: (user: User) => void;
}
export const useUserStore = create<UserStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
// Utilisation
export function UserProfile() {
const user = useUserStore((state) => state.user);
return <div>{user?.name}</div>;
}
```
**React Query (Server State):**
- Cache et synchronisation des données serveur
- Refetch, mutation, invalidation automatique
- Gestion optimiste
- Loading et error states
**Exemple:**
```typescript
const { data, isLoading, error } = useQuery({
queryKey: ['matches'],
queryFn: () => fetch('/api/matches').then(r => r.json()),
staleTime: 5000, // Données fraîches pendant 5s
});
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return <MatchList matches={data} />;
```
#### 5. Middleware
**Next.js Middleware:**
- Authentification (better-auth)
- Protection de routes
- Redirections
- Headers et cookies
**Exemple:**
```typescript
// middleware.ts
import { auth } from "@/lib/auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isOnDashboard = req.nextUrl.pathname.startsWith("/dashboard");
if (!isLoggedIn && isOnDashboard) {
return Response.redirect(new URL("/login", req.url));
}
});
export const config = {
matcher: ["/dashboard/:path*", "/api/auth/:path*"],
};
```
---
### Architecture des Composants
#### 1. Component Hierarchy
**Composition de composants:**
- Pages → Layouts → Sections → Components → UI Elements
**Exemple:**
```
MatchPage (Page)
└── MatchLayout (Layout)
├── MatchHeader (Section)
│ └── MatchTitle (Component)
├── MatchContent (Section)
│ ├── MatchInfo (Component)
│ ├── MatchStats (Component)
│ │ └── StatCard (UI Element)
│ └── MatchPredictions (Component)
│ └── PredictionCard (UI Element)
└── MatchFooter (Section)
```
#### 2. Component Patterns
**Container/Presentational Pattern:**
```typescript
// Container: Logique et state
export function MatchContainer() {
const { data, isLoading } = useMatch(matchId);
const { predict } = usePrediction();
if (isLoading) return <Loading />;
return <MatchView match={data} onPredict={predict} />;
}
// Presentational: UI pure
export function MatchView({ match, onPredict }: Props) {
return (
<div>
<MatchHeader match={match} />
<PredictButton onClick={() => onPredict(match.id)} />
</div>
);
}
```
**Compound Components Pattern (shadcn/ui):**
```typescript
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
</DialogHeader>
</DialogContent>
</Dialog>
```
#### 3. Custom Hooks
**Logique réutilisable:**
```typescript
// hooks/use-matches.ts
export function useMatches() {
return useQuery({
queryKey: ['matches'],
queryFn: fetchMatches,
});
}
// hooks/use-auth.ts
export function useAuth() {
const { user } = useUserStore();
return { isAuthenticated: !!user, user };
}
```
---
## Partie 2: Backend Architecture (backend/)
### Type d'Architecture: REST API / Layered Architecture
**Framework:** FastAPI
**Caractéristiques Principales:**
#### 1. Layered Architecture
**Couches:**
1. **API Layer** (`app/api/`)
- Endpoints REST
- Validation des requêtes
- Sérialisation des réponses
2. **Service Layer** (`app/services/`)
- Logique métier
- Orchestration des opérations
- Pas de dépendances directes aux modèles
3. **Repository/Model Layer** (`app/models/`)
- Accès aux données
- Opérations CRUD
- Abstraction de la base de données
4. **ML Layer** (`app/ml/`)
- Services de machine learning
- Analyse de sentiment
- Calcul de prédictions
5. **Scraper Layer** (`app/scrapers/`)
- Collecte de données externes
- Scraping Twitter/Reddit/RSS
- Normalisation des données
**Exemple de flux:**
```
Request: GET /api/matches/1
API Layer (matches.py)
↓ Validate request
↓ Call service
Service Layer (match_service.py)
↓ Get match from repository
↓ Calculate predictions (ML service)
↓ Calculate energy (sentiment service)
↓ Aggregate data
Repository Layer (match.py)
↓ Query database
↓ Return model
API Layer
↓ Serialize with Pydantic
↓ Return response
```
#### 2. Dependency Injection
**FastAPI Depends:**
- Injection de dépendances automatique
- Test facile avec mocks
- Gestion des cycles de vie
**Exemple:**
```python
# main.py
app = FastAPI()
# Dependency: Database session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Dependency: Service
def get_match_service(db: Session = Depends(get_db)):
return MatchService(db)
# Usage in endpoint
@app.get("/matches/{match_id}")
async def get_match(
match_id: int,
service: MatchService = Depends(get_match_service)
):
return service.get_match(match_id)
```
#### 3. Pydantic Validation
**Schémas de validation:**
```python
# schemas/match.py
from pydantic import BaseModel
class MatchCreate(BaseModel):
home_team: str
away_team: str
scheduled_date: datetime
league: str
class MatchResponse(BaseModel):
id: int
home_team: str
away_team: str
scheduled_date: datetime
predictions: List[PredictionResponse]
class Config:
orm_mode = True
```
**Utilisation:**
```python
@app.post("/matches", response_model=MatchResponse)
async def create_match(
match_data: MatchCreate,
db: Session = Depends(get_db)
):
# Validation automatique par Pydantic
match = Match(**match_data.dict())
db.add(match)
db.commit()
return match
```
#### 4. Async/Await
**Opérations asynchrones:**
```python
# scrapers/twitter_scraper.py
async def scrape_tweets(match_id: int):
# Opération async (non-bloquante)
tweets = await twitter_client.search_tweets(...)
return tweets
# main.py
@app.post("/scrape/{match_id}")
async def scrape_match(match_id: int):
tweets = await scrape_tweets(match_id)
return {"count": len(tweets)}
```
#### 5. Background Tasks & Workers
**Background Tasks (FastAPI):**
```python
from fastapi import BackgroundTasks
@app.post("/predict/{match_id}")
async def predict_match(
match_id: int,
background_tasks: BackgroundTasks
):
# Task en arrière-plan
background_tasks.add_task(
calculate_prediction,
match_id
)
return {"message": "Prediction started"}
```
**RabbitMQ Workers (Indépendants):**
```python
# workers/scraping_worker.py
def consume_scraping_tasks():
connection = pika.BlockingConnection(...)
channel = connection.channel()
channel.queue_declare(queue='scraping')
for method_frame, properties, body in channel.consume('scraping'):
task = json.loads(body)
scrape_match(task['match_id'])
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
```
---
## Architecture Intégrée (Frontend + Backend)
### Communication Pattern
**REST API Communication:**
```
Frontend Component
↓ React Query (Client)
↓ HTTP Fetch
Backend API Endpoint
↓ Pydantic Validation
↓ Service Layer
↓ Repository Layer
↓ Database (SQLAlchemy)
```
**Example Flow:**
```typescript
// Frontend: src/services/matches.ts
export function useMatches() {
return useQuery({
queryKey: ['matches'],
queryFn: () => fetch('/api/matches').then(r => r.json()),
});
}
```
```python
# Backend: app/api/matches.py
@app.get("/matches", response_model=List[MatchResponse])
async def get_matches(service: MatchService = Depends(get_match_service)):
return service.get_all_matches()
```
```python
# Backend: app/services/match_service.py
class MatchService:
def get_all_matches(self):
matches = self.db.query(Match).all()
# Add predictions, energy calculations
return matches
```
---
## Architecture Asynchrone (RabbitMQ)
### Pattern Producer-Consumer
**Queue Architecture:**
```
Frontend Request
Backend API (Producer)
↓ Publish to Queue
RabbitMQ
↓ Message
Worker (Consumer)
↓ Process Task
↓ Update Database
Database Update
Frontend Polling/Websocket
```
**Example Tasks:**
1. **Scraping Task:**
- Producer: API endpoint when user requests scrape
- Consumer: Scraping worker
- Task: Scrape Twitter/Reddit for match
2. **Analysis Task:**
- Producer: Scraping worker after data collected
- Consumer: Sentiment analysis worker
- Task: Analyze sentiment of collected tweets/posts
3. **Prediction Task:**
- Producer: Analysis worker after sentiment calculated
- Consumer: Prediction worker
- Task: Calculate prediction based on energy
---
## Architecture de Base de Données
### Phase 1: SQLite
**Schema:**
- Frontend: `chartbastan.db` (Drizzle ORM)
- Backend: Même database (SQLAlchemy)
**Tables:**
- users (authentification)
- matches (données de matchs)
- predictions (prédictions utilisateurs)
- tweets (données Twitter)
- posts (données Reddit)
- energy_scores (énergie collective)
### Phase 2: PostgreSQL
**Avantages:**
- Meilleure performance pour grandes quantités de données
- Concurrency supérieure
- Support avancé des requêtes
- Scalabilité horizontale
**Migration:**
- Même schema SQLite → PostgreSQL
- Alembic pour migrations
- Drizzle pour frontend
---
## Patterns de Sécurité
### Frontend Security
**better-auth:**
- JWT tokens
- Session management
- Password hashing (bcryptjs)
- CSRF protection
**Next.js Security:**
- Middleware pour auth
- Protected routes
- Secure cookies (httpOnly)
### Backend Security
**FastAPI Security:**
- JWT authentication
- Pydantic validation
- CORS configuration
- SQL injection prevention (SQLAlchemy)
**Example:**
```python
# middleware/auth.py
from fastapi import Security, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Security(security)
):
token = credentials.credentials
user = verify_token(token)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
@app.get("/protected")
async def protected_route(user = Depends(get_current_user)):
return {"user": user}
```
---
## Patterns de Performance
### Frontend Performance
**Code Splitting:**
- Automatic code splitting (Next.js)
- Dynamic imports for heavy components
- Route-based splitting
```typescript
// Dynamic import
const MatchDetail = dynamic(() => import('@/components/MatchDetail'), {
loading: () => <Loading />,
ssr: false,
});
```
**Optimistic Updates:**
```typescript
const mutation = useMutation({
mutationFn: predictMatch,
onMutate: async (newPrediction) => {
// Cancel pending queries
await queryClient.cancelQueries(['predictions']);
// Optimistic update
queryClient.setQueryData(['predictions'], (old) => [...old, newPrediction]);
},
onError: (err, newPrediction, context) => {
// Rollback on error
queryClient.setQueryData(['predictions'], context.previousPredictions);
},
});
```
**Streaming & Suspense:**
```typescript
export default async function Page() {
return (
<Suspense fallback={<Loading />}>
<MatchList />
</Suspense>
);
}
```
### Backend Performance
**Async Operations:**
```python
# Multiple async operations
tweets = await scrape_twitter(match_id)
posts = await scrape_reddit(match_id)
rss = await scrape_rss(match_id)
# Process in parallel
tweets, posts, rss = await asyncio.gather(
scrape_twitter(match_id),
scrape_reddit(match_id),
scrape_rss(match_id)
)
```
**Connection Pooling:**
```python
# SQLAlchemy
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20,
pool_pre_ping=True
)
```
**Caching:**
```python
from functools import lru_cache
@lru_cache(maxsize=128)
def get_prediction_model():
# Expensive operation, cached
return load_model()
```
---
## Résumé de l'Architecture
**Frontend (Next.js):**
- App Router (Server + Client Components)
- State: Zustand + React Query
- Data Fetching: SSR + Client-side
- UI: Tailwind + shadcn/ui
**Backend (FastAPI):**
- REST API Layered
- Service-oriented
- Async/await
- RabbitMQ workers
**Integration:**
- REST API communication
- Shared database (SQLite)
- Async processing with queues
**Architecture appropriée pour:**
- Application web moderne
- Real-time data
- Scaling horizontal
- Maintenance facile