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

16 KiB

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:

// 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:

// 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:

// 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:

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:

// 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:

// 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):

<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Title</DialogTitle>
    </DialogHeader>
  </DialogContent>
</Dialog>

3. Custom Hooks

Logique réutilisable:

// 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:

# 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:

# 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:

@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:

# 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):

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):

# 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:

// Frontend: src/services/matches.ts
export function useMatches() {
  return useQuery({
    queryKey: ['matches'],
    queryFn: () => fetch('/api/matches').then(r => r.json()),
  });
}
# 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()
# 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:

# 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
// Dynamic import
const MatchDetail = dynamic(() => import('@/components/MatchDetail'), {
  loading: () => <Loading />,
  ssr: false,
});

Optimistic Updates:

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:

export default async function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <MatchList />
    </Suspense>
  );
}

Backend Performance

Async Operations:

# 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:

# SQLAlchemy
engine = create_engine(
    DATABASE_URL,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True
)

Caching:

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