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.
← Réponse HTTP / HTTPS
WebSocket
gRPC
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éristique | Client | Serveur |
|---|---|---|
| Initiateur | ✓ Envoie les requêtes | Attend passivement* |
| Traitement | Affichage, UI | Données, logique métier |
| Stockage | Limité (localStorage, cookies) | Base de données, fichiers |
| Sécurité | Non fiable (code visible) | Fiable (code non exposé) |
| Exemples | Chrome, Firefox, Postman | Express, Flask, Nginx |
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
GET /articles HTTP/1.1Host: api.monsite.com · Accept: application/json · Authorization: Bearer token...
200 OKContent-Type: application/json · [{"id":1,"titre":"..."}]
| Méthode | Action | Corps | Idempotent |
|---|---|---|---|
| GET | Lire une ressource | Non | ✓ |
| POST | Créer une ressource | Oui | ✗ |
| PUT | Remplacer entièrement | Oui | ✓ |
| PATCH | Modifier partiellement | Oui | ✗ |
| DELETE | Supprimer | Non | ✓ |
── 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"
}
# 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
Chaque couche peut scaler indépendamment.
┌─────────────────────────────────────────┐
│ 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.
| URL | Méthode | Action | Retourne |
|---|---|---|---|
/articles | GET | Lister | Array d'articles |
/articles | POST | Créer | Nouvel article (201) |
/articles/42 | GET | Voir un | Article 42 |
/articles/42 | PUT | Remplacer | Article modifié |
/articles/42 | DELETE | Supprimer | 204 No Content |
/articles?page=2 | GET | Paginer | Page 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.
✗ 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
// 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()),
]);
# 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.
// 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);
// 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 REST | WebSocket | |
|---|---|---|
| Connexion | Ouvre/ferme à chaque requête | Persistante |
| Initiateur | Client seulement | Client ET serveur |
| Latence | Plus haute (TCP handshake) | Très basse |
| Complexité | Simple | Plus complexe |
| Usage | CRUD classique | Temps réel |
Serveur Python — Flask & FastAPI
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)
# 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
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 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
// 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' }),
};
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 = 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"])
| Menace | Protection |
|---|---|
| CORS non configuré | N'utiliser jamais origin: '*' en prod |
| Injection SQL | Toujours utiliser des requêtes préparées (?) |
| XSS | Ne pas injecter de HTML non nettoyé dans le DOM |
| CSRF | Tokens CSRF, SameSite cookies |
| Mots de passe en clair | bcrypt (coût ≥ 10) |
| Token JWT exposé | Ne jamais stocker dans localStorage pour des tokens sensibles — préférer httpOnly cookies |
| Rate limiting | express-rate-limit — limiter les requêtes par IP |
| HTTPS obligatoire | Let's Encrypt + redirection HTTP→HTTPS |
Cheat sheet Client-Serveur
🌐 HTTP — méthodes
| GET | Lire — pas de body |
| POST | Créer — body JSON |
| PUT | Remplacer entier |
| PATCH | Modifier partiel |
| DELETE | Supprimer |
📊 Codes de statut
| 200 | OK — GET réussi |
| 201 | Created — POST réussi |
| 204 | No Content — DELETE |
| 400 | Bad Request |
| 401 | Non authentifié |
| 403 | Pas les droits |
| 404 | Non trouvé |
| 500 | Erreur serveur |
🔗 Fetch JavaScript
fetch(url) | GET simple |
await r.json() | Parser la réponse |
r.ok | Status 200-299 ? |
r.status | Code 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_code | Code HTTP |
r.raise_for_status() | Lever si erreur |
headers={'Authorization': ...} | Bearer token |