Skip to content

Instantly share code, notes, and snippets.

@stephdl
Created February 23, 2026 13:56
Show Gist options
  • Select an option

  • Save stephdl/f4015235bf677220525da641f1776972 to your computer and use it in GitHub Desktop.

Select an option

Save stephdl/f4015235bf677220525da641f1776972 to your computer and use it in GitHub Desktop.
Architecture d'une Application Web Moderne

Architecture d'une Application Web Moderne

Vue.js (Frontend) · FastAPI/Python (Backend) · PostgreSQL

Documentation de référence pour comprendre et concevoir une application web full-stack robuste, scalable et maintenable.


Table des matières

  1. Vue d'ensemble de l'architecture
  2. Structure des fichiers
  3. Le Frontend Vue.js
  4. Le Backend Python (FastAPI)
  5. La Base de données PostgreSQL
  6. Les Routes (Routing)
  7. Les Middlewares
  8. L'Authentification
  9. La Communication Frontend ↔ Backend
  10. Gestion des erreurs
  11. Performances et Scalabilité
  12. Pièges courants et comment les éviter
  13. Checklist d'architecture

1. Vue d'ensemble de l'architecture

Le modèle client-serveur

Une application web moderne repose sur une séparation claire entre trois couches distinctes :

┌─────────────────────────────────────────────────────────┐
│  NAVIGATEUR (Client)                                    │
│  Vue.js SPA – rendu HTML/CSS/JS, état local, routing   │
└────────────────────┬────────────────────────────────────┘
                     │  HTTP/HTTPS (JSON, JWT)
                     │  WebSocket (temps réel)
┌────────────────────▼────────────────────────────────────┐
│  BACKEND (Serveur)                                      │
│  FastAPI/Python – logique métier, auth, validation      │
│  Middlewares – CORS, logging, rate limiting, sécurité   │
└────────────────────┬────────────────────────────────────┘
                     │  SQL (connexion pool)
┌────────────────────▼────────────────────────────────────┐
│  BASE DE DONNÉES                                        │
│  PostgreSQL – persistance, intégrité, transactions      │
└─────────────────────────────────────────────────────────┘

Pourquoi cette séparation ?

Cette architecture en couches (layered architecture) répond à plusieurs besoins fondamentaux :

  • Séparation des responsabilités : chaque couche a un rôle précis et ne connaît pas les détails internes des autres. Le frontend ne sait pas comment les données sont stockées ; le backend ne sait pas comment elles sont affichées.
  • Scalabilité indépendante : on peut déployer plus d'instances du backend sans toucher au frontend, ou migrer la base de données sans refaire l'API.
  • Maintenabilité : une équipe frontend et une équipe backend peuvent travailler en parallèle à partir d'un contrat d'interface (l'API).
  • Sécurité : la base de données n'est jamais exposée directement à Internet.

Le flux d'une requête typique

Prenons un cas concret : un utilisateur consulte la liste de ses commandes.

  1. L'utilisateur navigue vers /orders dans le navigateur.
  2. Vue Router intercepte cette navigation et charge le composant OrdersView.vue.
  3. Le composant, au montage, appelle une fonction du store Pinia qui envoie une requête GET /api/orders au backend.
  4. La requête transporte un JWT dans son header Authorization.
  5. Le backend reçoit la requête. Le middleware d'authentification valide le JWT.
  6. Le middleware de logging enregistre la requête.
  7. La route /api/orders est trouvée, elle appelle la fonction de contrôleur correspondante.
  8. Le contrôleur interroge PostgreSQL via l'ORM.
  9. Les données sont sérialisées en JSON et renvoyées avec le code HTTP 200.
  10. Le store Pinia met à jour son état avec les données reçues.
  11. Vue réagit au changement d'état et met à jour le DOM automatiquement.

2. Structure des fichiers

Vue d'ensemble du monorepo

L'organisation des fichiers est un choix architectural en soi. Voici une structure recommandée qui favorise la clarté et la scalabilité :

projet/
├── frontend/                  # Application Vue.js
│   ├── public/
│   │   └── favicon.ico
│   ├── src/
│   │   ├── assets/            # Images, polices, CSS globaux
│   │   ├── components/        # Composants réutilisables (UI)
│   │   │   ├── ui/            # Composants atomiques (Button, Input...)
│   │   │   └── layout/        # Header, Footer, Sidebar...
│   │   ├── views/             # Pages entières (liées au router)
│   │   │   ├── HomeView.vue
│   │   │   ├── auth/
│   │   │   │   ├── LoginView.vue
│   │   │   │   └── RegisterView.vue
│   │   │   └── dashboard/
│   │   │       └── DashboardView.vue
│   │   ├── router/
│   │   │   └── index.js       # Définition des routes et guards
│   │   ├── stores/            # Pinia stores (état global)
│   │   │   ├── auth.js        # Authentification, token, user
│   │   │   └── orders.js      # Données métier
│   │   ├── services/          # Couche d'accès à l'API
│   │   │   ├── api.js         # Instance axios configurée
│   │   │   ├── auth.service.js
│   │   │   └── orders.service.js
│   │   ├── composables/       # Logique réutilisable (hooks Vue)
│   │   │   ├── useAuth.js
│   │   │   └── usePagination.js
│   │   ├── utils/             # Fonctions utilitaires pures
│   │   └── main.js            # Point d'entrée
│   ├── .env                   # Variables d'environnement locales
│   ├── .env.production        # Variables pour la production
│   └── vite.config.js
│
├── backend/                   # API Python (FastAPI)
│   ├── app/
│   │   ├── api/               # Couche transport HTTP
│   │   │   ├── v1/
│   │   │   │   ├── routes/    # Définition des endpoints
│   │   │   │   │   ├── auth.py
│   │   │   │   │   └── orders.py
│   │   │   │   └── __init__.py
│   │   │   └── deps.py        # Dépendances injectables (get_db, get_user...)
│   │   ├── core/              # Configuration centrale
│   │   │   ├── config.py      # Settings (Pydantic BaseSettings)
│   │   │   ├── security.py    # Fonctions JWT, hashing
│   │   │   └── middleware.py  # Middlewares personnalisés
│   │   ├── db/
│   │   │   ├── base.py        # Base SQLAlchemy
│   │   │   ├── session.py     # Engine, SessionLocal
│   │   │   └── migrations/    # Alembic
│   │   ├── models/            # Modèles SQLAlchemy (ORM)
│   │   │   ├── user.py
│   │   │   └── order.py
│   │   ├── schemas/           # Schémas Pydantic (validation I/O)
│   │   │   ├── user.py
│   │   │   └── order.py
│   │   ├── services/          # Logique métier
│   │   │   ├── auth.service.py
│   │   │   └── order.service.py
│   │   └── main.py            # Point d'entrée FastAPI
│   ├── tests/
│   ├── .env
│   ├── alembic.ini
│   └── requirements.txt
│
├── docker-compose.yml         # Orchestration locale
└── README.md

Pourquoi cette organisation ?

La séparation routes / services / models / schemas est fondamentale.

  • models/ : représente la structure de la base de données. C'est la source de vérité pour le schéma SQL.
  • schemas/ : représente les données qui entrent et sortent de l'API. Pas toujours identiques aux modèles (on ne renvoie jamais le mot de passe haché par exemple).
  • services/ : contient la logique métier. Une règle simple : si une logique dépasse 5 lignes dans une route, elle doit aller dans un service.
  • routes/ : ne fait que orchestrer — valider l'entrée avec les schemas, appeler le service, retourner la réponse.

Cette organisation permet de tester la logique métier indépendamment du framework HTTP.


3. Le Frontend Vue.js

Architecture en composants

Vue.js repose sur un modèle de composants. Chaque composant est une unité autonome qui encapsule son template HTML, sa logique JavaScript et parfois son style CSS.

La hiérarchie typique est :

App.vue
└── Layout (RouterView)
    ├── Header.vue
    ├── [Page] View.vue        ← Composant de page (chargé par Vue Router)
    │   ├── DataTable.vue      ← Composant de présentation
    │   │   └── TableRow.vue
    │   └── FilterPanel.vue
    └── Footer.vue

Cas d'usage réel : une page de tableau de bord e-commerce. La DashboardView.vue ne contient pas de HTML de tableau. Elle orchestre des composants <StatsCard>, <RecentOrders>, <RevenueChart>. Chacun de ces composants est indépendant, testable, et réutilisable sur d'autres pages.

Pinia : la gestion d'état

Pinia est le gestionnaire d'état officiel de Vue 3. Il répond à la question : "Comment partager des données entre des composants qui ne sont pas dans une relation parent-enfant ?"

Un store Pinia représente un domaine de données de l'application. Il contient :

  • State : les données brutes (l'utilisateur connecté, la liste des commandes).
  • Getters : des données dérivées calculées (le nombre de commandes en attente, le nom complet de l'utilisateur).
  • Actions : les fonctions qui modifient l'état, incluant les appels API asynchrones.

Règle d'or : ne jamais faire d'appel API directement dans un composant Vue. Toujours passer par une action de store ou un service. Cela centralise la logique et facilite le débogage.

Vue Router et la navigation

Vue Router gère la navigation côté client sans recharger la page (SPA — Single Page Application). Le routing SPA intercepte les clics sur les liens et change le composant affiché sans faire de requête HTTP au serveur pour une nouvelle page HTML.

Les concepts clés sont :

  • Routes statiques : /about, /login
  • Routes dynamiques : /orders/:id — le :id est un paramètre récupérable dans le composant
  • Routes imbriquées (nested routes) : permettent des layouts partagés avec sous-navigation
  • Navigation Guards : des fonctions exécutées avant/après une navigation pour contrôler l'accès (voir section Authentification)

4. Le Backend Python (FastAPI)

Pourquoi FastAPI ?

FastAPI est un framework web Python moderne qui génère automatiquement une documentation interactive (Swagger UI) à partir de vos annotations de type. Il est asynchrone par nature (async/await), ce qui lui permet de gérer efficacement un grand nombre de requêtes concurrentes sans bloquer.

L'injection de dépendances

C'est le concept le plus important à comprendre dans FastAPI. Le système de Depends() permet de déclarer des ressources partagées (connexion à la base de données, utilisateur authentifié) et de les injecter automatiquement dans les fonctions de route.

Pourquoi c'est puissant :

  • La connexion à la base de données est ouverte automatiquement au début de la requête et fermée à la fin, même en cas d'erreur.
  • Pour protéger une route, on ajoute simplement current_user = Depends(get_current_user) dans la signature de la fonction.
  • Les dépendances sont facilement mockables dans les tests.

Cas d'usage réel : une plateforme SaaS multi-tenant. Une dépendance get_current_tenant résout automatiquement le tenant à partir du JWT, et toutes les requêtes en base de données sont filtrées en conséquence. Aucune route n'a besoin de gérer cette logique manuellement.

Les schémas Pydantic

Pydantic valide et sérialise les données. En FastAPI, on définit un schéma pour les données en entrée (request body) et un autre pour les données en sortie (response).

Requête JSON entrante
    ↓
Pydantic valide et convertit les types (string → datetime, etc.)
    ↓
Fonction de route reçoit un objet Python typé
    ↓
Service traite la donnée
    ↓
Modèle SQLAlchemy (ORM) ↔ PostgreSQL
    ↓
Pydantic sérialise la réponse (en excluant les champs sensibles)
    ↓
Réponse JSON sortante

Piège courant : utiliser le même schéma pour la création (UserCreate), la mise à jour (UserUpdate) et la lecture (UserResponse). Ce sont trois schémas distincts car leurs champs diffèrent : la création inclut le mot de passe, la réponse ne l'inclut jamais, la mise à jour a tous les champs optionnels.


5. La Base de données PostgreSQL

ORM vs SQL brut

Un ORM (Object-Relational Mapper) comme SQLAlchemy traduit les classes Python en tables SQL. Il évite d'écrire du SQL répétitif et protège contre les injections SQL.

Quand utiliser l'ORM :

  • CRUD standard (Create, Read, Update, Delete)
  • Requêtes avec jointures simples
  • La majorité des cas d'usage quotidiens

Quand écrire du SQL brut :

  • Requêtes complexes avec des agrégations, des fenêtres (WINDOW), des CTEs récursives
  • Requêtes de reporting avec des performances critiques
  • Migrations de données

Règle : l'ORM est votre outil principal, le SQL brut est votre outil de précision.

Les migrations avec Alembic

Une migration est un fichier versionné qui décrit comment évoluer le schéma de la base de données. Alembic, l'outil de migration de SQLAlchemy, génère automatiquement ces fichiers en comparant vos modèles Python avec l'état actuel de la base.

Règle fondamentale : ne jamais modifier manuellement le schéma d'une base de production. Toujours passer par une migration. Cela garantit que tous les environnements (dev, staging, prod) ont le même schéma, et qu'on peut revenir en arrière (downgrade) en cas de problème.

Le connection pooling

Chaque requête HTTP ne doit pas ouvrir une nouvelle connexion à PostgreSQL — c'est une opération coûteuse. Le connection pool maintient un ensemble de connexions ouvertes et les réutilise.

SQLAlchemy gère un pool par défaut. En production, il faut dimensionner ce pool selon le nombre de workers du serveur et la limite de connexions de PostgreSQL.

Cas d'usage réel : une API qui reçoit 500 requêtes/seconde avec un pool de 20 connexions. SQLAlchemy met en file d'attente les requêtes qui ne peuvent pas obtenir une connexion immédiatement, plutôt que d'en créer de nouvelles indéfiniment.


6. Les Routes (Routing)

Convention de nommage des routes (REST)

REST (REpresentational State Transfer) est une convention d'organisation des URL. Une URL représente une ressource, et le verbe HTTP exprime l'action.

Ressource : orders (commandes)

GET    /api/v1/orders          → Liste toutes les commandes
POST   /api/v1/orders          → Crée une nouvelle commande
GET    /api/v1/orders/{id}     → Récupère une commande spécifique
PUT    /api/v1/orders/{id}     → Remplace entièrement une commande
PATCH  /api/v1/orders/{id}     → Met à jour partiellement une commande
DELETE /api/v1/orders/{id}     → Supprime une commande

Ressource imbriquée :
GET    /api/v1/orders/{id}/items   → Items d'une commande spécifique

Pourquoi /api/v1/ ? Le préfixe /api distingue les routes de l'API des routes qui serviraient des pages HTML. Le /v1 permet de faire évoluer l'API sans casser les clients existants : on peut déployer /v2 en parallèle et migrer progressivement.

Organisation des routes dans FastAPI

FastAPI utilise des APIRouter pour regrouper les routes par domaine fonctionnel. Chaque domaine a son propre fichier de routes, et tous sont enregistrés dans main.py avec un préfixe.

Cas d'usage réel : une application avec des domaines users, orders, products, inventory. Chaque domaine a son router avec ses propres dépendances, ses propres tags Swagger, et ses propres préfixes. L'équipe backend peut travailler sur orders.py et products.py en parallèle sans conflit.

Les codes de réponse HTTP

Choisir le bon code HTTP n'est pas accessoire — c'est un contrat avec le client.

2xx - Succès
  200 OK          → Requête réussie (GET, PUT, PATCH)
  201 Created     → Ressource créée (POST)
  204 No Content  → Succès sans corps de réponse (DELETE)

4xx - Erreur client (la requête est incorrecte)
  400 Bad Request       → Données invalides
  401 Unauthorized      → Non authentifié (pas de token)
  403 Forbidden         → Authentifié mais non autorisé
  404 Not Found         → Ressource inexistante
  409 Conflict          → Conflit (email déjà utilisé)
  422 Unprocessable     → Erreur de validation (FastAPI par défaut)
  429 Too Many Requests → Rate limiting

5xx - Erreur serveur (le serveur a failli)
  500 Internal Server Error → Erreur non gérée
  503 Service Unavailable   → Serveur temporairement indisponible

Piège courant : retourner 200 OK avec un message d'erreur dans le corps (ex: {"success": false, "error": "Not found"}). C'est une pratique à éviter — les codes HTTP sont faits pour ça, et les clients (et les load balancers) les utilisent.


7. Les Middlewares

Qu'est-ce qu'un middleware ?

Un middleware est une couche de traitement qui s'intercale entre la requête entrante et la route finale (et entre la réponse de la route et le client). Il forme une chaîne : chaque middleware peut inspecter, modifier, bloquer ou laisser passer la requête.

Requête entrante
    ↓
[Middleware: Logging]       → enregistre la requête
    ↓
[Middleware: Rate Limiting]  → vérifie les quotas
    ↓
[Middleware: CORS]           → ajoute les headers CORS
    ↓
[Middleware: Auth]           → valide le JWT (si applicable)
    ↓
Route Handler               → exécute la logique métier
    ↓
[Middleware: Compression]    → compresse la réponse
    ↓
Réponse sortante

L'ordre des middlewares est critique. Le logging doit être en premier pour capturer toutes les requêtes. Le rate limiting avant l'auth pour ne pas faire de travail coûteux sur des requêtes à bloquer. CORS avant tout traitement métier.

CORS (Cross-Origin Resource Sharing)

Le CORS est un mécanisme de sécurité du navigateur. Par défaut, un navigateur refuse qu'une page sur https://app.monsite.com envoie une requête à https://api.monsite.com — ce sont deux origines différentes (port ou domaine différent).

Le middleware CORS du backend répond en ajoutant des headers HTTP qui indiquent au navigateur quelles origines sont autorisées.

Piège courant : configurer allow_origins=["*"] (tout autoriser) en production. C'est acceptable uniquement pour une API publique sans authentification. Pour une API avec des cookies ou des tokens d'authentification, il faut spécifier explicitement les origines autorisées.

Cas d'usage réel : en développement local, le frontend tourne sur http://localhost:5173 et le backend sur http://localhost:8000. Sans CORS configuré, aucune requête ne fonctionne. On ajoute http://localhost:5173 aux origines autorisées uniquement en développement.

Rate Limiting

Le rate limiting protège l'API contre les abus : bots, attaques par force brute, clients mal codés qui bouclent indéfiniment.

Il existe deux niveaux :

  • Par IP : limite le nombre de requêtes depuis une adresse IP donnée. Protège contre les attaques externes.
  • Par utilisateur : limite par token JWT ou clé API. Permet des quotas différenciés (plan gratuit vs premium).

Implémentation : en production, le rate limiting est souvent géré par un reverse proxy (Nginx, Traefik) ou une API Gateway (Kong, AWS API Gateway) plutôt que dans l'application elle-même. Cela évite que des requêtes refusées consomment des ressources applicatives.

Logging et Observabilité

Un bon système de logging est indispensable pour déboguer en production. Chaque requête devrait enregistrer :

  • La méthode HTTP et l'URL
  • Le code de réponse
  • Le temps de traitement
  • L'identifiant de l'utilisateur (anonymisé si nécessaire)
  • Un identifiant de corrélation (correlation_id) pour tracer une requête à travers plusieurs services

Structurez vos logs en JSON : cela les rend parsables par des outils d'agrégation (Loki, Elasticsearch). Un log en texte libre est difficile à filtrer sur 10 millions de lignes.


8. L'Authentification

JWT (JSON Web Token) : le mécanisme fondamental

Un JWT est un jeton cryptographiquement signé qui prouve l'identité d'un utilisateur. Il contient des claims (affirmations) : l'identifiant utilisateur, son rôle, la date d'expiration.

Structure d'un JWT :

eyJhbGciOiJIUzI1NiJ9   ← Header (algorithme)
.
eyJ1c2VyX2lkIjoxMjN9  ← Payload (claims)
.
SflKxwRJSMeKKF2QT4fw  ← Signature (vérification d'intégrité)

Le backend signe le JWT avec une clé secrète. Quand le client renvoie ce JWT, le backend vérifie la signature pour s'assurer qu'il n'a pas été modifié. Le backend n'a pas besoin de consulter la base de données pour chaque requête — le JWT est auto-porteur.

Access Token et Refresh Token

Problème : si un JWT a une longue durée de vie (30 jours) et qu'il est volé, l'attaquant a 30 jours d'accès. Si la durée est courte (15 minutes), l'utilisateur doit se reconnecter toutes les 15 minutes — expérience désastreuse.

Solution : deux tokens complémentaires.

Access Token
  → Durée courte (15 minutes)
  → Envoyé dans chaque requête API
  → Stocké en mémoire (store Pinia) — jamais dans localStorage

Refresh Token
  → Durée longue (7-30 jours)
  → Utilisé uniquement pour obtenir un nouvel Access Token
  → Stocké dans un cookie HttpOnly Secure — inaccessible au JavaScript

Flux de rafraîchissement :

  1. Le client envoie une requête avec son Access Token.
  2. Le backend répond 401 Unauthorized (token expiré).
  3. Un intercepteur Axios capte ce 401 et envoie automatiquement une requête POST /auth/refresh avec le Refresh Token (depuis le cookie).
  4. Le backend valide le Refresh Token et renvoie un nouveau couple Access/Refresh.
  5. La requête originale est rejouée avec le nouveau token.
  6. L'utilisateur ne voit rien — tout est transparent.

Stockage sécurisé des tokens

C'est l'un des points les plus critiques et les plus mal compris.

localStorage / sessionStorage
  ✗ Accessible par tout JavaScript de la page
  ✗ Vulnérable aux attaques XSS
  ✗ NE PAS utiliser pour les tokens d'authentification

Cookie HttpOnly Secure SameSite=Strict
  ✓ Inaccessible au JavaScript (protection XSS)
  ✓ Envoyé automatiquement par le navigateur
  ✓ SameSite=Strict protège contre le CSRF
  ✓ Recommandé pour le Refresh Token

Mémoire (store Pinia)
  ✓ Inaccessible au JavaScript externe
  ✓ Perdu au rafraîchissement de la page (normal pour un Access Token)
  ✓ Recommandé pour l'Access Token

Piège courant : stocker le JWT dans localStorage parce que "c'est plus simple". C'est un vecteur d'attaque XSS classique. Une injection de script malveillant peut voler tous les tokens en quelques lignes.

Hachage des mots de passe

Les mots de passe ne doivent jamais être stockés en clair ni en MD5/SHA1. Ces algorithmes sont rapides, ce qui les rend vulnérables aux attaques par dictionnaire.

Utilisez bcrypt ou Argon2 — des algorithmes conçus pour être lents, avec un facteur de coût configurable. Argon2 est le choix moderne recommandé par les compétitions de cryptographie.

Autorisation : RBAC (Role-Based Access Control)

L'authentification répond à "qui es-tu ?". L'autorisation répond à "qu'as-tu le droit de faire ?".

Le RBAC associe des rôles aux utilisateurs (admin, manager, user) et des permissions aux rôles.

Utilisateur → a un ou plusieurs Rôles → un Rôle possède des Permissions

Alice (role: admin)   → peut tout faire
Bob (role: manager)   → peut lire et modifier les commandes
Carol (role: user)    → peut seulement lire ses propres commandes

Cas d'usage réel : une plateforme RH. Les managers voient les salaires de leur équipe, les employés voient seulement le leur. Un endpoint GET /employees/{id}/salary vérifie dans le service : est-ce que current_user est le propriétaire des données, ou son manager, ou un admin ?


9. La Communication Frontend ↔ Backend

Axios : la couche de service

Axios est le client HTTP recommandé pour Vue.js. Il faut le configurer une seule fois dans services/api.js et exporter une instance partagée.

Configuration essentielle de l'instance Axios :

  • baseURL : l'URL de base de l'API (depuis les variables d'environnement)
  • Intercepteur de requête : ajoute automatiquement le JWT dans le header Authorization: Bearer <token> avant chaque requête
  • Intercepteur de réponse : intercepte les 401 pour tenter un refresh de token, redirige vers /login si le refresh échoue, gère les erreurs réseau globalement

Variables d'environnement

Ne jamais écrire l'URL de l'API en dur dans le code. Vite expose les variables préfixées par VITE_ aux composants.

.env.development    →  VITE_API_URL=http://localhost:8000
.env.production     →  VITE_API_URL=https://api.monsite.com

REST vs WebSocket : quel protocole choisir ?

HTTP REST est une communication requête-réponse : le client initie, le serveur répond, la connexion se ferme.

WebSocket est une connexion persistante bidirectionnelle : serveur et client peuvent s'envoyer des messages à tout moment.

Utilisez REST pour :
  → CRUD classique (commandes, utilisateurs, produits)
  → Authentification
  → Opérations ponctuelles

Utilisez WebSocket pour :
  → Chat en temps réel
  → Notifications push ("votre commande est livrée")
  → Tableaux de bord avec données en temps réel (cours boursiers)
  → Jeux en ligne, édition collaborative

Cas d'usage hybride : une application de suivi de livraison. Le chargement initial de la page (liste des commandes) utilise REST. Une fois la page chargée, une connexion WebSocket écoute les mises à jour de statut en temps réel. Si le WebSocket se déconnecte, l'application se rabat sur du polling (interrogation toutes les 30 secondes).

Pagination, filtrage et recherche

Ne jamais retourner toutes les données d'une table. Une table avec 1 million de lignes retournée en une seule requête fera planter l'API et le navigateur.

Stratégies de pagination :

  • Offset/Limit : GET /orders?page=3&per_page=20. Simple à implémenter mais lent sur de grandes tables (la base de données doit compter tous les éléments précédents).
  • Cursor-based : GET /orders?cursor=<id_dernier_element>. Plus performant sur de grandes tables, idéal pour le scroll infini. Impossible d'aller directement à la "page 42".

10. Gestion des erreurs

Côté Backend : exceptions centralisées

FastAPI permet de définir des handlers d'exceptions globaux. Plutôt que de gérer les erreurs dans chaque route, on lève des exceptions métier (UserNotFound, OrderAlreadyCancelled) et des handlers centralisés les convertissent en réponses HTTP standardisées.

Structure de réponse d'erreur standardisée :

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "L'utilisateur avec l'ID 123 n'existe pas.",
    "details": null,
    "request_id": "abc-123-xyz"
  }
}

Un request_id dans la réponse d'erreur est précieux : le client peut l'inclure dans un rapport de bug, et vous pouvez retrouver exactement cette requête dans vos logs.

Côté Frontend : erreurs globales vs locales

Erreurs globales (réseau, 401, 500) : gérées dans l'intercepteur Axios. Affichées via un système de notifications toast.

Erreurs locales (validation de formulaire, 404 sur une page spécifique) : gérées dans le composant ou le store concerné. Affichées inline dans l'interface.

Piège courant : afficher le message d'erreur brut du serveur à l'utilisateur ("FOREIGN_KEY_VIOLATION on table orders"). Définissez des messages d'erreur utilisateur dans le frontend, et utilisez le code d'erreur (USER_NOT_FOUND) pour le mapping.


11. Performances et Scalabilité

Indexation de la base de données

Un index PostgreSQL accélère les recherches sur une colonne au prix d'un espace disque supplémentaire et d'écritures légèrement plus lentes.

Règles pratiques :

  • Toujours indexer les colonnes de clés étrangères (user_id, order_id).
  • Indexer les colonnes fréquemment utilisées dans les clauses WHERE.
  • Indexer les colonnes de tri (created_at, updated_at).
  • Utiliser EXPLAIN ANALYZE en PostgreSQL pour identifier les requêtes lentes (seq scan sur une grande table = index manquant).

Mise en cache

Problème : une requête populaire (GET /products avec 10 000 produits) est appelée 1000 fois par minute. Chaque appel interroge la base de données.

Solution : Redis comme cache en mémoire.

Flux avec cache :
  Requête → Cache Redis ?
                ├─ OUI → retourne les données (< 1ms)
                └─ NON → interroge PostgreSQL
                          → stocke dans Redis (TTL 5 min)
                          → retourne les données

Stratégies d'invalidation du cache : c'est le problème le plus difficile du cache. Deux approches :

  • TTL (Time-To-Live) : les données expirent après un délai fixe. Simple mais peut retourner des données légèrement obsolètes.
  • Invalidation sur écriture : quand un produit est modifié, on supprime son entrée du cache. Données toujours fraîches mais logique plus complexe.

Scalabilité horizontale

Scalabilité verticale : augmenter les ressources d'un seul serveur (plus de RAM, plus de CPU). Limite physique, coûteux.

Scalabilité horizontale : ajouter plus d'instances du serveur derrière un load balancer.

Internet
    ↓
[Load Balancer] (Nginx, Traefik, AWS ALB)
    ├── Instance Backend #1
    ├── Instance Backend #2
    └── Instance Backend #3
          ↓
      [PostgreSQL] (partagé)
      [Redis] (partagé)

Prérequis pour la scalabilité horizontale :

  • L'application doit être stateless : aucune donnée de session en mémoire locale. L'état est dans la base de données ou Redis.
  • Les fichiers uploadés doivent être dans un stockage partagé (S3, MinIO), pas sur le disque du serveur.
  • Les tâches en arrière-plan utilisent une file de messages (Celery + Redis/RabbitMQ), pas des threads locaux.

Workers asynchrones (Celery)

Certaines opérations sont trop lentes pour être faites dans une requête HTTP (envoi d'email, génération de PDF, traitement d'image, appels à des APIs tierces lentes).

Requête HTTP → Route FastAPI → Enfile la tâche dans Redis/RabbitMQ → Répond 202 Accepted immédiatement

                               ↓ (asynchrone)
                    Worker Celery exécute la tâche
                    Résultat stocké en base de données
                    Notification WebSocket ou email à l'utilisateur

12. Pièges courants et comment les éviter

Sécurité

Injection SQL : toujours utiliser l'ORM ou des requêtes paramétrées. Ne jamais concaténer des inputs utilisateur dans une chaîne SQL.

Exposition de données sensibles : définir des schémas de réponse stricts (Pydantic) qui excluent explicitement les champs sensibles (mot de passe haché, tokens internes, données d'autres utilisateurs). Ne pas utiliser le modèle ORM directement comme réponse API.

CORS mal configuré : en production, spécifier explicitement les origines autorisées. Ne pas utiliser * avec des credentials.

Secrets dans le code : utiliser des variables d'environnement et un fichier .env (non commité). Utiliser un gestionnaire de secrets (Vault, AWS Secrets Manager) en production.

Performance

N+1 queries : le piège classique des ORM. Si vous chargez 100 commandes et que pour chaque commande vous faites une requête pour charger l'utilisateur, vous obtenez 101 requêtes au lieu d'1. Solution : utiliser joinedload ou selectinload dans SQLAlchemy pour charger les relations en une seule requête.

Pas de pagination : retourner toutes les données d'une table sans limite. Implémenter systématiquement la pagination sur tous les endpoints de liste.

Requêtes lentes non détectées : activer le logging des requêtes lentes dans PostgreSQL (log_min_duration_statement) pour identifier les problèmes avant qu'ils n'atteignent la production.

Architecture

Logique métier dans les routes : les fonctions de route doivent être courtes et orchestrer, pas implémenter. Si une route dépasse 20 lignes, quelque chose doit aller dans un service.

Couplage fort frontend/backend : ne pas écrire les URL de l'API en dur dans chaque composant. Centraliser dans des fichiers service. Si l'URL change, vous n'avez qu'un seul endroit à modifier.

Migrations non gérées : modifier le schéma de la base de données manuellement en production. Toujours utiliser Alembic. Toujours tester les migrations (et les downgrade) sur un environnement de staging avant la production.

Pas de versioning d'API : déployer une v2 de l'API qui casse la compatibilité avec les clients existants. Préfixer dès le départ avec /v1 et maintenir les anciennes versions le temps que les clients migrent.


13. Checklist d'architecture

Avant de passer en production, vérifiez ces points :

Sécurité

  • Mots de passe hachés avec bcrypt ou Argon2
  • JWT avec expiration courte + refresh token en cookie HttpOnly
  • CORS configuré avec des origines spécifiques
  • Variables d'environnement pour tous les secrets
  • HTTPS uniquement en production
  • Rate limiting sur les endpoints d'authentification
  • Validation des inputs avec Pydantic
  • Schémas de réponse qui excluent les données sensibles

Performance

  • Indexes sur les clés étrangères et colonnes de recherche fréquentes
  • Pagination sur tous les endpoints de liste
  • Connection pooling configuré
  • Cache pour les données fréquemment lues et rarement modifiées
  • Tâches longues déléguées à des workers asynchrones

Qualité

  • Variables d'environnement pour les URLs d'API (pas d'URL en dur)
  • Migrations Alembic pour tous les changements de schéma
  • Logging structuré avec request_id
  • Gestion des erreurs centralisée (frontend et backend)
  • Versioning d'API (/v1)

Scalabilité

  • Application stateless (pas de session en mémoire locale)
  • Fichiers uploadés dans un stockage objet (S3/MinIO)
  • Health check endpoint (GET /health)

Documentation rédigée pour une stack Vue.js 3 (Composition API) + FastAPI + PostgreSQL. Outils de référence : Pinia, Vue Router, Axios, SQLAlchemy, Alembic, Pydantic, Redis, Celery.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment