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 routelayout.tsx- Layout partagé pour la route et ses enfantsloading.tsx- UI de chargement (automatic streaming)error.tsx- UI d'erreur (error boundary)not-found.tsx- UI pour 404route.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:
-
API Layer (
app/api/)- Endpoints REST
- Validation des requêtes
- Sérialisation des réponses
-
Service Layer (
app/services/)- Logique métier
- Orchestration des opérations
- Pas de dépendances directes aux modèles
-
Repository/Model Layer (
app/models/)- Accès aux données
- Opérations CRUD
- Abstraction de la base de données
-
ML Layer (
app/ml/)- Services de machine learning
- Analyse de sentiment
- Calcul de prédictions
-
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:
-
Scraping Task:
- Producer: API endpoint when user requests scrape
- Consumer: Scraping worker
- Task: Scrape Twitter/Reddit for match
-
Analysis Task:
- Producer: Scraping worker after data collected
- Consumer: Sentiment analysis worker
- Task: Analyze sentiment of collected tweets/posts
-
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