Architecture logicielle

Client
↔ Serveur

Le modèle fondamental du web — comprendre comment un navigateur dialogue avec un serveur, HTTP, les APIs, la synchronisation et le temps réel.

C'est quoi l'architecture Client-Serveur ?

Le modèle client-serveur divise une application en deux rôles distincts : le client fait des demandes, le serveur répond. C'est le fondement de tout le web — et de la plupart des applications réseau.

🖥️ Client
Navigateur web
Application mobile
Application desktop
Script Python / curl
Autre serveur (microservice)
Requête →

← Réponse
HTTP / HTTPS
WebSocket
gRPC
⚙️ Serveur
Serveur web (Express, Flask)
Serveur de fichiers (Nginx)
Serveur de base de données
Serveur de mails
Serveur de jeux
📜

Le modèle client-serveur est apparu dans les années 1960 avec les mainframes et les terminaux. Il a remplacé le modèle monolithique où tout tournait sur une seule machine. Le web (1991) en est l'application la plus répandue.

CaractéristiqueClientServeur
Initiateur✓ Envoie les requêtesAttend passivement*
TraitementAffichage, UIDonnées, logique métier
StockageLimité (localStorage, cookies)Base de données, fichiers
SécuritéNon fiable (code visible)Fiable (code non exposé)
ExemplesChrome, Firefox, PostmanExpress, Flask, Nginx
* Sauf avec WebSocket — le serveur peut alors initier des messages

Protocole HTTP — la langue du web

HTTP (HyperText Transfer Protocol) est le protocole de communication entre client et serveur. Chaque échange = une requête du client + une réponse du serveur.

Anatomie d'un échange HTTP

→ 1
Requête clientGET /articles HTTP/1.1
Host: api.monsite.com · Accept: application/json · Authorization: Bearer token...
⚙ 2
Traitement serveur — lecture en BDD, calculs, validation…
← 3
Réponse serveur200 OK
Content-Type: application/json · [{"id":1,"titre":"..."}]
MéthodeActionCorpsIdempotent
GETLire une ressourceNon
POSTCréer une ressourceOui
PUTRemplacer entièrementOui
PATCHModifier partiellementOui
DELETESupprimerNon
Exemple de requête/réponse HTTP brute
── REQUÊTE ──────────────────────────────
POST /api/articles HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: Bearer eyJhbGci...

{
  "titre": "Mon premier article",
  "contenu": "Bonjour le monde !"
}

── RÉPONSE ──────────────────────────────
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/articles/42

{
  "id": 42,
  "titre": "Mon premier article",
  "contenu": "Bonjour le monde !",
  "createdAt": "2024-03-15T10:30:00Z"
}
Codes de statut HTTP essentiels
# 2xx — Succès
200 OK          # GET réussi
201 Created     # POST → ressource créée
204 No Content  # DELETE réussi (pas de body)

# 3xx — Redirection
301 Moved Permanently  # URL changée définitivement
302 Found              # Redirection temporaire

# 4xx — Erreur client
400 Bad Request    # Corps invalide / champ manquant
401 Unauthorized   # Non authentifié
403 Forbidden      # Authentifié, mais pas les droits
404 Not Found      # Ressource inexistante
422 Unprocessable  # Validation échouée

# 5xx — Erreur serveur
500 Internal Server Error  # Bug côté serveur
503 Service Unavailable    # Serveur surchargé / en maintenance

1-tier, 2-tier, 3-tier, N-tier

🖥️ 1-tier (monolithique)
Tout sur la même machine : interface, logique et données. Exemple : Excel, un script Python autonome. Simple mais non distribuable.
🖥️ ↔ ⚙️ 2-tier (client-serveur classique)
Client (interface) + Serveur (données). Exemple : application desktop qui se connecte à une BDD MySQL. Le client peut contenir de la logique métier.
🖥️ ↔ ⚙️ ↔ 🗄️ 3-tier (web moderne)
Présentation (navigateur / app mobile) ↔ Logique métier (API Express/Flask) ↔ Données (BDD MySQL/MongoDB).
Chaque couche peut scaler indépendamment.
🌐 N-tier (microservices)
Chaque fonctionnalité = un service indépendant. Ex : service auth, service articles, service mails. Complexe mais très scalable. Netflix, Uber, Amazon.
Architecture 3-tier typique d'un projet HEH
┌─────────────────────────────────────────┐
│           TIER 1 — Présentation          │
│  Navigateur : Vue.js / React             │
│  → affiche les données                   │
│  → envoie les actions de l'utilisateur   │
└────────────────┬────────────────────────┘
                 │  HTTP/JSON (fetch, axios)
                 ▼
┌─────────────────────────────────────────┐
│           TIER 2 — Logique métier        │
│  API Express / Flask                     │
│  → authentification (JWT)                │
│  → validation des données                │
│  → règles métier                         │
│  → orchestration                         │
└────────────────┬────────────────────────┘
                 │  SQL / ORM
                 ▼
┌─────────────────────────────────────────┐
│           TIER 3 — Données               │
│  MySQL / PostgreSQL / MongoDB            │
│  → stockage persistant                   │
│  → transactions                          │
│  → contraintes d'intégrité              │
└─────────────────────────────────────────┘
💡

L'architecture 3-tier est la cible standard pour les projets de fin d'études à la HEH. Elle correspond exactement à la stack Vue.js + Node.js/Express + MySQL ou React + FastAPI + PostgreSQL.

API REST — contrat client-serveur

Une API REST est une façon standardisée de définir les échanges entre client et serveur. Elle repose sur les méthodes HTTP et des URLs qui représentent des ressources.

URLMéthodeActionRetourne
/articlesGETListerArray d'articles
/articlesPOSTCréerNouvel article (201)
/articles/42GETVoir unArticle 42
/articles/42PUTRemplacerArticle modifié
/articles/42DELETESupprimer204 No Content
/articles?page=2GETPaginerPage 2
📐

Principes REST : sans état (chaque requête est indépendante), ressources identifiées par des URLs, représentation en JSON/XML, opérations standardisées via les méthodes HTTP.

Bonnes URL REST vs mauvaises
✗ Mauvaises pratiques (verbes dans l'URL)
GET /getArticles
GET /getArticleById?id=42
POST /createArticle
POST /deleteArticle?id=42

✓ Bonnes pratiques (noms, verbes HTTP)
GET    /articles
GET    /articles/42
POST   /articles
DELETE /articles/42

✓ Relations imbriquées
GET    /utilisateurs/1/articles    # articles de l'utilisateur 1
POST   /utilisateurs/1/articles    # créer un article pour user 1
DELETE /utilisateurs/1/articles/5  # supprimer article 5 de user 1

✓ Versions d'API
GET /api/v1/articles
GET /api/v2/articles   # nouvelle version sans casser l'ancienne

✓ Filtres via query string
GET /articles?categorie=tech&publie=true&page=2&limite=10

Synchrone vs Asynchrone

Client JavaScript — fetch async/await
// Appel asynchrone — le navigateur n'est PAS bloqué
async function chargerArticles() {
  try {
    const reponse = await fetch('/api/articles');
    if (!reponse.ok) throw new Error(`HTTP ${reponse.status}`);
    const articles = await reponse.json();
    afficher(articles);
  } catch (err) {
    console.error('Erreur réseau :', err);
  }
}

// POST avec corps JSON
async function creerArticle(data) {
  const reponse = await fetch('/api/articles', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json',
               'Authorization': `Bearer ${token}` },
    body:    JSON.stringify(data),
  });
  return reponse.json();
}

// Plusieurs requêtes en parallèle
const [articles, utilisateurs] = await Promise.all([
  fetch('/api/articles').then(r => r.json()),
  fetch('/api/utilisateurs').then(r => r.json()),
]);
Client Python — requests (synchrone)
# pip install requests
import requests

BASE = 'http://localhost:3000/api'
token = 'eyJhbGci...'
headers = {'Authorization': f'Bearer {token}'}

# GET — lire
rep = requests.get(f'{BASE}/articles', headers=headers)
rep.raise_for_status()  # lève une exception si 4xx/5xx
articles = rep.json()

# POST — créer
rep = requests.post(f'{BASE}/articles',
    json={'titre': 'Test', 'contenu': '...'},
    headers=headers
)
nouvel_article = rep.json()

# Asynchrone Python — httpx
# pip install httpx
import httpx, asyncio

async def charger():
    async with httpx.AsyncClient() as client:
        rep = await client.get(f'{BASE}/articles')
        return rep.json()

asyncio.run(charger())
💡

requests est synchrone — le script Python attend la réponse avant de continuer. Pour des appels en parallèle (ex. scraper), utiliser httpx ou aiohttp avec asyncio.

WebSocket — communication temps réel

HTTP est request-response — le client demande, le serveur répond, la connexion se ferme. WebSocket maintient une connexion persistante bidirectionnelle : le serveur peut envoyer des données au client sans qu'il les demande.

🔄

Cas d'usage : chat en temps réel, notifications push, cours boursiers en live, tableau de bord temps réel, jeux multijoueurs, collaboration type Google Docs.

Serveur WebSocket — Node.js avec Socket.io
// npm install socket.io
const http = require('http');
const { Server } = require('socket.io');
const express = require('express');

const app    = express();
const serveur = http.createServer(app);
const io     = new Server(serveur, {
  cors: { origin: '*' }
});

io.on('connection', socket => {
  console.log(`Client connecté : ${socket.id}`);

  // Recevoir un message du client
  socket.on('message', data => {
    console.log('Reçu :', data);
    // Envoyer à tous les clients connectés
    io.emit('message', { ...data, id: socket.id });
  });

  socket.on('rejoindre-salon', salon => {
    socket.join(salon);                      // groupe de clients
    io.to(salon).emit('info', 'Nouveau membre');
  });

  socket.on('disconnect', () => {
    console.log(`Déconnexion : ${socket.id}`);
  });
});

serveur.listen(3000);
Client WebSocket — JavaScript
// Dans le navigateur — Socket.io client
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

// Événements de connexion
socket.on('connect', () => {
  console.log('Connecté !', socket.id);
  socket.emit('rejoindre-salon', 'salon-general');
});

// Recevoir un message du serveur
socket.on('message', data => {
  afficherMessage(data);
});

// Envoyer un message au serveur
function envoyer(texte) {
  socket.emit('message', {
    texte,
    auteur: 'Alice',
    horodatage: new Date().toISOString()
  });
}

socket.on('disconnect', () => console.log('Déconnecté'));
HTTP RESTWebSocket
ConnexionOuvre/ferme à chaque requêtePersistante
InitiateurClient seulementClient ET serveur
LatencePlus haute (TCP handshake)Très basse
ComplexitéSimplePlus complexe
UsageCRUD classiqueTemps réel

Serveur Python — Flask & FastAPI

Flask — API REST serveur
from flask import Flask, request, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # autoriser les appels depuis le front

articles = [
    {'id': 1, 'titre': 'Premier', 'contenu': '...'}
]

@app.route('/api/articles', methods=['GET'])
def lister():
    return jsonify(articles), 200

@app.route('/api/articles', methods=['POST'])
def creer():
    data = request.get_json()
    if not data.get('titre'):
        return jsonify({'erreur': 'Titre requis'}), 400
    nouvel = {'id': len(articles) + 1, **data}
    articles.append(nouvel)
    return jsonify(nouvel), 201

@app.route('/api/articles/<int:id>', methods=['DELETE'])
def supprimer(id):
    global articles
    articles = [a for a in articles if a['id'] != id]
    return '', 204

if __name__ == '__main__':
    app.run(debug=True, port=5000)
FastAPI — alternative moderne avec types
# pip install fastapi uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"],
                   allow_methods=["*"], allow_headers=["*"])

class ArticleIn(BaseModel):
    titre:   str
    contenu: str

class Article(ArticleIn):
    id: int

articles: list[Article] = []

@app.get("/api/articles")  # doc auto sur /docs !
def lister() -> list[Article]:
    return articles

@app.post("/api/articles", status_code=201)
def creer(data: ArticleIn) -> Article:
    nouvel = Article(id=len(articles)+1, **data.dict())
    articles.append(nouvel)
    return nouvel

@app.delete("/api/articles/{id}", status_code=204)
def supprimer(id: int):
    global articles
    articles = [a for a in articles if a.id != id]

# Lancer : uvicorn main:app --reload
💡

FastAPI génère automatiquement une documentation interactive sur /docs (Swagger UI) — très utile pour tester l'API sans Postman.

Serveur Node.js — Express

Express — API REST complète
const express = require('express');
const cors    = require('cors');
const app = express();

app.use(cors());
app.use(express.json());

let articles = [{ id: 1, titre: 'Premier' }];
let nextId = 2;

app.get('/api/articles', (req, res) => {
  const { categorie, page = 1, limite = 10 } = req.query;
  let resultats = articles;
  if (categorie) resultats = resultats.filter(a => a.categorie === categorie);
  res.json({ data: resultats.slice((page-1)*limite, page*limite), total: resultats.length });
});

app.get('/api/articles/:id', (req, res) => {
  const article = articles.find(a => a.id === Number(req.params.id));
  if (!article) return res.status(404).json({ erreur: 'Non trouvé' });
  res.json(article);
});

app.post('/api/articles', (req, res) => {
  const { titre, contenu } = req.body;
  if (!titre) return res.status(400).json({ erreur: 'Titre requis' });
  const article = { id: nextId++, titre, contenu };
  articles.push(article);
  res.status(201).json(article);
});

app.delete('/api/articles/:id', (req, res) => {
  articles = articles.filter(a => a.id !== Number(req.params.id));
  res.status(204).send();
});

app.listen(3000, () => console.log('Port 3000'));
Tester l'API — curl & fetch
# Tester avec curl (terminal)
curl http://localhost:3000/api/articles

curl -X POST http://localhost:3000/api/articles \
  -H "Content-Type: application/json" \
  -d '{"titre":"Test","contenu":"..."}'

curl -X DELETE http://localhost:3000/api/articles/1

// Tester avec fetch (console navigateur)
const r = await fetch('http://localhost:3000/api/articles');
const data = await r.json();
console.log(data);

// Outils recommandés pour tester les API :
// - Thunder Client (extension VS Code, gratuit)
// - Postman (desktop, gratuit)
// - Insomnia (desktop, gratuit)
// - Hoppscotch (web, gratuit)
🔧

Thunder Client est directement intégré dans VS Code — c'est l'outil recommandé pour tester vos API pendant le développement.

Client JavaScript — consommer une API

Service API réutilisable (vanilla JS)
// api.js — couche d'abstraction sur fetch
const BASE = 'http://localhost:3000/api';

async function requete(url, options = {}) {
  const token = localStorage.getItem('token');
  const reponse = await fetch(BASE + url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options.headers,
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  });

  if (reponse.status === 204) return null;  // pas de body
  if (!reponse.ok) {
    const erreur = await reponse.json();
    throw new Error(erreur.message || `Erreur ${reponse.status}`);
  }
  return reponse.json();
}

export const ArticleAPI = {
  lister:     ()         => requete('/articles'),
  voir:       (id)       => requete(`/articles/${id}`),
  creer:      (data)     => requete('/articles', { method: 'POST',   body: data }),
  modifier:   (id, data) => requete(`/articles/${id}`, { method: 'PUT',    body: data }),
  supprimer:  (id)       => requete(`/articles/${id}`, { method: 'DELETE' }),
};
Utilisation dans un composant Vue
import { ArticleAPI } from '../api';

// Charger
const articles = await ArticleAPI.lister();

// Créer
const nouvel = await ArticleAPI.creer({
  titre: 'Nouveau', contenu: 'Contenu...'
});

// Supprimer
await ArticleAPI.supprimer(42);
⚠️

Ne jamais faire confiance au client ! Toute validation côté client (JavaScript) peut être contournée par l'utilisateur. La vraie validation doit toujours être faite côté serveur. Le client valide pour l'UX, le serveur valide pour la sécurité.

Sécurité & CORS

CORS — comprendre et configurer
// CORS = Cross-Origin Resource Sharing
// Le navigateur bloque par défaut les requêtes vers un
// domaine différent de la page (sécurité same-origin policy)
//
// Ex : front sur http://localhost:5173
//      back  sur http://localhost:3000
// → Le navigateur bloque sans CORS configuré !
//
// Solution : le SERVEUR autorise explicitement les origines

// Node.js — cors middleware
const cors = require('cors');
app.use(cors({
  origin:  ['http://localhost:5173', 'https://monsite.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

# Python Flask
from flask_cors import CORS
CORS(app, origins=["http://localhost:5173"])
MenaceProtection
CORS non configuréN'utiliser jamais origin: '*' en prod
Injection SQLToujours utiliser des requêtes préparées (?)
XSSNe pas injecter de HTML non nettoyé dans le DOM
CSRFTokens CSRF, SameSite cookies
Mots de passe en clairbcrypt (coût ≥ 10)
Token JWT exposéNe jamais stocker dans localStorage pour des tokens sensibles — préférer httpOnly cookies
Rate limitingexpress-rate-limit — limiter les requêtes par IP
HTTPS obligatoireLet's Encrypt + redirection HTTP→HTTPS

Cheat sheet Client-Serveur

🌐 HTTP — méthodes

GETLire — pas de body
POSTCréer — body JSON
PUTRemplacer entier
PATCHModifier partiel
DELETESupprimer

📊 Codes de statut

200OK — GET réussi
201Created — POST réussi
204No Content — DELETE
400Bad Request
401Non authentifié
403Pas les droits
404Non trouvé
500Erreur serveur

🔗 Fetch JavaScript

fetch(url)GET simple
await r.json()Parser la réponse
r.okStatus 200-299 ?
r.statusCode HTTP
method: 'POST'Changer la méthode
body: JSON.stringify()Envoyer du JSON
Promise.all([])Requêtes en parallèle

🐍 Requests Python

requests.get(url)GET
requests.post(url, json={})POST JSON
r.json()Parser la réponse
r.status_codeCode HTTP
r.raise_for_status()Lever si erreur
headers={'Authorization': ...}Bearer token