Au-delà de CRUD : dix habitudes architecturales pour maintenir les applications MERN fonctionnelles
Apprenez les habitudes au niveau du système qui permettent à une application MERN de rester en bon état tout en grandissant : la propriété des données, les contrats API, l’état dérivé, le code en couches, des chargements légers et des erreurs cohérentes.
La plupart des tutoriels MERN se terminent par une application CRUD fonctionnelle : MongoDB stocke les données, Express expose quelques routes, React affiche une liste, et il peut y avoir un écran de connexion. Cette application fonctionne, mais elle survit rarement inchangée face à son développement. À mesure que de nouvelles fonctionnalités et des collaborateurs s’ajoutent, les API deviennent difficiles à modifier, l’état perd sa synchronisation, les pages ralentissent, et le débogage prend plus de temps que la création elle-même. Les outils ne sont rarement la cause. Ce guide présente dix habitudes architecturales permettant de résoudre ces problèmes, afin que vous puissiez considérer une application MERN comme un système unique plutôt que comme quatre bibliothèques distinctes.
Considérez MERN comme un flux de données, et non comme une liste d’outils
La description habituelle de cette stack se limite à ses composants :
MongoDB + Express + React + Node.js
C’est exact, mais cela ne dit rien sur la manière dont les composants coopèrent, un peu comme décrire une voiture en mentionnant seulement son moteur, ses quatre roues et son volant. Une représentation plus utile montre comment les données se déplacent de l’utilisateur vers la base de données, puis en retour :
User
│
▼
React
│
HTTP
│
▼
Express + Node
│
Database Queries
│
▼
MongoDB
Chaque flèche représente une frontière avec ses propres règles : ce que le navigateur peut envoyer, ce que le serveur accepte, ce que la base de données stocke. La plupart des bonnes pratiques décrites ci-dessous concernent la détermination de ce qui se passe à ces frontières.
1. Donner un responsable unique à chaque donnée
Lors du développement d’une nouvelle fonctionnalité, demandez d’abord à qui appartiennent les données concernées. Dans de nombreux projets jeunes, la réponse n’est pas claire. Les informations de l’utilisateur actuel peuvent se trouver dans l’état d’un composant, dans un store Redux, dans localStorage, dans une réponse API récente ou dans un autre cache, en même temps. Tôt ou tard, l’une de ces copies reste en retard par rapport aux autres, ce qui fait que l’interface affiche deux versions contradictoires du même fait.
Une répartition des responsabilités plus claire :
- MongoDB est la source de vérité pour les données persistées.
- Le backend gère les règles métier qui déterminent comment ces données peuvent être modifiées.
- Le frontend affiche les données et demande des modifications via l’API ; toute copie côté client n’est qu’un cache, pas une source autorisée.
Le principe directeur est que chaque donnée possède exactement une source unique de vérité, et toutes les autres copies savent qu’elles peuvent être obsolètes. Les bibliothèques de gestion de l’état du serveur existent principalement pour gérer ce cachement de manière explicite ; consultez rethinking server state with React Query and Redux pour en savoir plus sur cet aspect du problème.
2. Concevoir des API en tant que contrats
Un premier endpoint typique ressemble à ceci :
app.get("/users", async (req, res) => {
const users = await User.find();
res.json(users);
});
Cela fonctionne, mais cela promet également implicitement que la réponse sera toujours un tableau de documents d’utilisateur complets, quelles que soient les champs du modèle. Une fois qu’une application mobile, un tableau de bord, une intégration avec un partenaire ou une autre équipe dépendent de cette structure, la modifier peut les mettre en panne.
Au préalable d’ajouter un endpoint, décidez :
- quels champs il renvoie précisément, plutôt que de restituer le modèle brut (ce qui peut également divulguer des champs internes ou sensibles) ;
- s’il est possible de le modifier ultérieurement sans endommager les clients, ou s’il nécessite une gestion des versions ;
- quels autres systèmes sont susceptibles de l’utiliser.
3. Stocker le moins d’état React possible
React est présenté comme une bibliothèque d’interface utilisateur, mais dans les applications réelles, la majeure partie des difficultés concerne l’état. Une erreur fréquente consiste à conserver dans l’état des valeurs qui pourraient être calculées :
const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);
Ici, filteredUsers est entièrement déterminé par users. Le stocker séparément signifie que chaque mise à jour doit maintenir les deux en synchronisation, et oublier cela entraîne une liste obsolète. Il vaut mieux le calculer pendant le rendu :
const filteredUsers = users.filter(user => user.active);
La règle est de ne stocker que ce que l’on ne peut pas calculer et d’obtenir le reste par dérivation. Si une telle dérivation devient réellement coûteuse, useMemo peut la mettre en cache, mais il s’agit toujours de données dérivées et non d’une seconde source de vérité.
4. N’oubliez pas que CRUD est la partie facile
De nombreux projets s’arrêtent aux quatre opérations de base :
Create
Read
Update
Delete
Un backend en production enveloppe ces opérations dans bien plus : validation, authentification, autorisation, règles métier, limitation des fréquences d’accès, traçabilité des actions, journalisation et notifications. Comparez une création naïve :
await User.create(req.body);
à une version qui vérifie d’abord les entrées
if (!isValid(req.body))
throw new Error("Invalid input");
et qui s’assure que l’appelant a le droit d’agir avant d’écrire :
if (!canCreateUser(req.user))
throw new Error("Unauthorized");await User.create(req.body);
La version naïve présente également un risque d’affectation massive : en passant directement req.body à create, un client peut définir n’importe quel champ accepté par le schéma, y compris une flag comme role. Il faut valider et sélectionner explicitement les champs autorisés. Notez également qu’une vérification de permissions échouée correspond conceptuellement à un code 403 (interdit), et non à 401, quel que soit le message d’erreur affiché. Écrire des données est simple ; les protéger, c’est là que l’ingénierie backend devient difficile.
5. Séparer les préoccupations en couches
La différence la plus évidente entre un codebase amateur et un codebase professionnel réside dans l’emplacement de la logique. Mélanger les préoccupations entraîne des gestionnaires de routes remplis de requêtes SQL, des composants React chargés de règles de validation et des contrôleurs saturés de logique métier, avec pour conséquence une croissance continue de chacun de ces fichiers.
Un backend en couches attribue une tâche par couche :
Routes
│
Controllers
│
Services
│
Repositories
│
Database
Les routes associent les URL aux gestionnaires d’actions, les contrôleurs transforment les requêtes HTTP en appels de fonctions, les services contiennent les règles métier, et les repositories communiquent avec la base de données. Les unités petites et à vocation unique sont plus faciles à tester et à modifier. Pour une explication plus détaillée, consultez la conception en couches d’une API Node.js.
6. Corriger d’abord les problèmes de performance à la source des données
Lorsqu’on leur demande comment accélérer une application React, la plupart des développeurs recourent à useMemo, React.memo et useCallback. Ces outils aident, mais de nombreux problèmes de performance commencent avant même que React ne soit impliqué. Imaginez une requête comme celle-ci :
GET /users
qui renvoie
50,000 users
alors que l’écran ne montre que
10 users
Aucune forme de mémorisation ne compense l’envoi et le traitement de dizaines de milliers d’enregistrements inutiles. Il faut y remédier à la source :
- pager les résultats ;
- filtrer sur le serveur ;
- n’afficher que les champs nécessaires au client ;
- compresser les réponses ;
- mettre en cache de manière intentionnelle, avec un plan d’invalidation clair.
Le composant le plus rapide à afficher est celui qui ne reçoit jamais de données qu’il n’a pas besoin.
7. Faire du traitement des erreurs une partie de la conception
Le code de développement absorbe souvent ce type d’erreurs :
try {
...
}
catch(error){
console.log(error);
}
En enregistrant les erreurs puis en continuant, on cache l’échec au client ainsi qu’aux outils de surveillance. Les API en production nécessitent des erreurs cohérentes et lisibles par les machines :
return res.status(400).json({
message: "Invalid email address",
code: "INVALID_EMAIL"
});
Une structure stable, accompagnée d’un message lisible par les humains et d’un code lisible par les machines, permet au frontend de relier les codes à des messages UI spécifiques, de regrouper les journaux par code, d’activer des alertes en cas de fréquences anormales, et de commencer le débogage à partir d’une catégorie connue plutôt qu’à partir d’un trace d’exécution. Les échecs sont inévitables ; l’objectif est de les gérer de manière prévisible.
8. Organiser le code par fonctionnalité
Avec vingt fichiers, n’importe quelle structure de dossiers convient. Avec cinq cents, cela devient très important. Un agencement regroupé par type technique répartit une fonctionnalité sur l’ensemble de l’arborescence :
routes/
controllers/
models/
Le regroupement par fonctionnalité permet de rassembler tout ce qui concerne un domaine donné en un seul endroit :
users/
routes.js
controller.js
service.js
validation.js
orders/
routes.js
controller.js
service.js
Lorsque la logique des commandes change, on ouvre uniquement le dossier orders. Les dossiers dédiés aux fonctionnalités facilitent également la définition des responsabilités, les revues de code et le transfert éventuel vers des services distincts.
9. Penser en termes de systèmes, pas de tickets
Une demande de fonctionnalité comme « ajouter un système d’authentification » peut être traitée de manière limitée, avec un formulaire et une route. Une approche axée sur le système amène à se poser des questions plus globales : comment l’authentification fonctionne du début à la fin, où sont stockés les tokens, comment les permissions sont appliquées, que se passe-t-il lorsque un token expire, et comment un futur client mobile s’authentifiera. Répondre à ces questions dès le départ demande un peu plus d’efforts aujourd’hui, mais évite des réécritures ultérieures.
10. Choisir délibérément les compromis
Aucune architecture n’est optimale dans toutes les situations. Chaque option apporte des avantages mais comporte aussi des inconvénients :
- Architecture simple : plus rapide à mettre en place, mais plus difficile à échaler.
- Microservices : échelle indépendante, mais complexité opérationnelle bien plus élevée.
- État global : partage facile entre les composants, mais débogage plus difficile.
- Conception de base de données normalisée : moins de doublons, mais davantage de jointures ou de recherches.
- Cachage agressif : réponses plus rapides, mais problème persistant de l’invalidation du cache.
Les bons ingénieurs ne sont pas ceux qui connaissent tous les patterns, mais ceux qui savent expliquer quand chaque pattern en vaut la peine.
Comment les projets MERN en croissance se dégradent généralement
Étape 1 : tout est simple
La première version couvre l’essentiel :
CRUD
Authentication
Dashboard
Deployment
Le code est petit, et tout le monde le comprend.
Étape 2 : la croissance révèle des raccourcis
Davantage d’utilisateurs, de fonctionnalités et de développeurs arrivent. Une logique redondante, des points d’entrée incohérents, des pages lentes, un état complexe et un débogage difficile apparaissent partout dans le codebase.
Étape 3 : les outils sont blâmés
L’équipe en conclut que React ne permet pas une expansion suffisante ou que le choix de MongoDB a été une erreur. En général, aucune de ces affirmations n’est vraie. L’architecture n’a tout simplement jamais évolué en même temps que l’application.
Une analogie de restaurant pour les couches
Pensez à la pile comme à un restaurant. MongoDB est l’entrepôt qui stocke tous les ingrédients. Express et Node constituent la cuisine : ils décident de ce qui sera cuisiné, de la manière dont cela sera préparé et de qui a le droit de passer commande. React est l’accueil, qui présente les plats prêts aux clients. Les clients n’ont pas besoin de savoir comment fonctionne la cuisine, et la cuisine se moque de la façon dont les assiettes sont disposées sur la table. Chaque partie fait bien son travail, ce qui correspond précisément à la séparation nécessaire dans une application MERN.
Points clés
Comprendre MERN ne consiste pas tant à écrire des requêtes, des routes et des composants, mais plutôt à visualiser les parcours suivis par les données, à placer les règles métier au bon niveau, à faire évoluer les API sans endommager les clients, à maintenir un état minimal et à reconnaître comment les décisions prises tôt ont des effets cumulatifs.
- Attribuez un responsable à chaque élément de données et considérez toutes les autres copies comme des caches.
Lorsqu’une nouvelle fonctionnalité apparaît, la question la plus utile n’est pas de savoir comment la construire, mais à qui revient chaque responsabilité.
Lectures complémentaires
- Dix habitudes d’architecture permettant de conserver des codebases frontend maintenables pendant des années — Explique des habitudes structurelles telles que l’optimisation pour la supprimabilité, un flux de données explicite et l’isolation de la logique métier, qui aident les codebases à rester maintenables au fil des années de changements.
- Les erreurs d’architecture backend qui compliquent la vie des équipes React privilégiant le frontend — Explique cinq défauts de conception backend fréquents dans les projets menés avec React, allant de l’utilisation incorrecte du paradigme API aux déploiements fragiles, ainsi que les solutions architecturales pour assurer une fiabilité de niveau production.
- Localhost n’est pas en production : diagnostiquer les apps React qui échouent lors du déploiement — Découvrez pourquoi une app React qui fonctionne bien sur votre machine échoue une fois déployée, et comment retracer les URLs API, les variables d’environnement, CORS, le routage, les ressources et l’authentification jusqu’à la couche responsable de l’échec.