Enzo Ruffin

PortFolio 2026

Stack MERN : le guide complet

MongoDB, Express, React, Node : à quoi sert chaque brique, qui fait le front, qui fait le back, et comment tout ça communique. Le guide MERN de A à Z.

19 min de lecture
Enzo Ruffin
Développement

MERN, ce sont quatre outils qu'on utilise ensemble : MongoDB, Express, React et Node.js. Pris séparément, chacun fait une chose précise. Mis bout à bout, ils suffisent à construire une application web complète, du bouton sur lequel l'utilisateur clique jusqu'à la ligne enregistrée en base de données. La première moitié de ce guide explique à quoi sert chaque brique, laquelle tourne dans le navigateur, laquelle tourne sur le serveur, et surtout comment elles se parlent. La seconde entre dans le détail : ce sont les décisions qui font la différence entre un projet qui tient trois ans et un prototype à jeter au bout de six mois.

Les quatre lettres, en une minute

MongoDB stocke les données. Node.js permet d'exécuter du JavaScript ailleurs que dans un navigateur. Express s'appuie sur Node pour recevoir les requêtes HTTP et y répondre. React affiche l'interface chez l'utilisateur. Trois de ces briques vivent sur le serveur, une seule vit dans le navigateur. Leur point commun, c'est le langage : on écrit du JavaScript partout, ce qui évite de changer de syntaxe et de façon de penser toutes les deux heures. C'est confortable, mais ce n'est pas le vrai bénéfice. Le vrai bénéfice, c'est de pouvoir partager du code entre les deux côtés : les règles de validation d'un formulaire, la forme d'un objet, les types.

MongoDB : la base de données

Une base de données conserve les informations quand le programme s'arrête : les comptes, les commandes, les messages. MongoDB range ces informations sous forme de documents, qui ressemblent beaucoup à des objets JavaScript. Un document client contient son nom, son email, son adresse, éventuellement la liste de ses préférences. Ces documents sont regroupés dans des collections, l'équivalent des tables en SQL. La grosse différence avec une base classique, c'est qu'il n'y a pas de colonnes fixes : deux documents d'une même collection peuvent ne pas avoir exactement les mêmes champs. Ça rend les débuts très rapides, puisqu'on n'écrit aucune migration pour ajouter une information. Ça se paie plus tard si personne ne décide de la forme des données, et c'est bien pour ça qu'on pose quand même un schéma par-dessus, avec Mongoose.

Node.js : du JavaScript en dehors du navigateur

Historiquement, le JavaScript ne tournait que dans une page web. Node.js a sorti le moteur du navigateur pour en faire un programme comme un autre, capable de lire des fichiers, d'ouvrir un port réseau ou de parler à une base de données. C'est ce qui permet d'écrire un serveur en JavaScript. Node apporte aussi npm, le catalogue de bibliothèques où on installe à peu près tout le reste. Une chose à retenir dès le premier jour : Node traite les requêtes sur un seul fil d'exécution. Il est excellent pour attendre, une réponse de la base ou un appel à une API externe, et mauvais pour calculer longtemps. Un gros traitement bloque tout le monde, y compris les autres utilisateurs connectés au même moment.

Express : ce qui transforme Node en serveur web

Node sait ouvrir une connexion, mais il ne connaît pas grand-chose au web. Express ajoute la couche qui manque : les routes. Une route associe une adresse et une méthode HTTP à un morceau de code. GET /api/commandes renvoie la liste, POST /api/commandes en crée une, GET /api/commandes/42 renvoie la commande 42. Entre la requête et la réponse, on intercale des middlewares, des fonctions qui s'exécutent dans l'ordre pour lire le corps de la requête, vérifier un jeton d'authentification, journaliser l'appel ou refouler une adresse trop bavarde. L'ensemble forme une API : une liste d'adresses qui répondent du JSON et jamais des pages HTML. C'est ce qui permet à un site, à une application mobile et à un back-office de taper sur le même serveur sans le dupliquer.

React : l'interface, côté navigateur

React construit l'écran. On découpe l'interface en composants, de petites fonctions qui décrivent un morceau de page : un bouton, une carte produit, un formulaire. Chaque composant peut avoir un état, c'est-à-dire des valeurs qui changent au fil de l'utilisation, comme le contenu d'un champ ou l'ouverture d'un menu. Quand l'état change, React recalcule ce qui doit bouger à l'écran, et lui seul. On n'écrit jamais « va chercher cet élément dans la page et modifie-le », on décrit ce que la page doit afficher pour un état donné. Tout ce code s'exécute chez l'utilisateur. C'est une règle simple et sans exception : ce qui est dans React est public. Aucune clé secrète, aucun mot de passe, aucune règle métier sensible n'a sa place là.

Qui est le front, qui est le back

Le front, c'est React : ce que l'utilisateur voit et manipule. Le back, c'est Node, Express et MongoDB, tout ce qui tourne sur une machine que l'utilisateur ne verra jamais. La frontière entre les deux n'est pas décorative. Le front peut être lu, modifié et rejoué par n'importe qui avec les outils de développement du navigateur. Le back est le seul endroit où une vérification a de la valeur. Si le front cache le bouton « supprimer » aux non-administrateurs, très bien, mais c'est le serveur qui doit refuser la suppression quand la requête arrive quand même. Cette règle explique à elle seule la moitié des failles qu'on trouve dans les projets de débutants.

Le modèle : décrire la forme des données

Côté serveur, on ne parle pas directement à MongoDB. On passe par Mongoose, qui sert de traducteur. On y déclare un schéma : le nom des champs, leur type, ce qui est obligatoire, ce qui doit être unique. À partir de ce schéma, Mongoose fabrique un modèle, l'objet dont on se sert pour créer, chercher et mettre à jour des documents. Chaque document reçoit au passage un identifiant unique, l'_id, généré automatiquement. C'est lui qu'on retrouve dans les URL et dans les liens entre collections : une commande contient l'_id de son client plutôt qu'une copie du client. Un schéma bien écrit fait aussi office de documentation. Quand quelqu'un rejoint le projet, c'est le premier fichier qu'il ouvre.

La route : l'adresse qui déclenche du code

Une route Express reçoit trois sortes d'informations. Les paramètres d'URL désignent une ressource précise, comme l'identifiant dans /api/commandes/42. La query, après le point d'interrogation, sert à filtrer ou paginer, par exemple ?statut=payee&page=2. Et le corps de la requête transporte les données envoyées lors d'une création ou d'une modification. Le code qui répond s'appelle un contrôleur, et son travail est court : lire ces trois entrées, appeler la fonction métier qui fait le vrai travail, renvoyer un statut et du JSON. Un contrôleur qui contient vingt lignes de calcul est un contrôleur qu'il faudra copier-coller le jour où la même action devra être déclenchée par une tâche automatique ou un webhook.

L'API : le contrat entre les deux mondes

Le front et le back ne partagent aucune mémoire. Ils s'échangent des messages, et ces messages suivent des conventions. Le verbe dit l'intention : GET pour lire, POST pour créer, PATCH pour modifier, DELETE pour supprimer. Le code de statut dit ce qui s'est passé : 200 quand tout va bien, 201 après une création, 400 quand la requête est mal formée, 401 quand on n'est pas connecté, 403 quand on n'a pas le droit, 404 quand la ressource n'existe pas, 500 quand c'est le serveur qui a fauté. Le corps, lui, est du JSON. Ce contrat mérite d'être stable, parce que le site, l'application mobile et les éventuelles intégrations extérieures l'apprennent par cœur. Renommer un champ sans prévenir casse des choses qu'on ne voit pas depuis son écran.

Le voyage d'une donnée, du clic à l'écran

Un utilisateur clique sur « Valider la commande ». React envoie une requête POST vers /api/commandes avec le contenu du panier en JSON et le jeton d'authentification. La requête arrive sur le serveur Node. Express la fait passer dans ses middlewares : lecture du corps, vérification du jeton, contrôle du format. La route appelle le contrôleur, qui appelle le service. Le service applique les règles, le stock est-il suffisant, ce client a-t-il le droit de commander, puis demande au modèle Mongoose d'enregistrer le document. Mongoose traduit tout ça en requête MongoDB, la base écrit et renvoie le document créé. Le service le transforme en réponse propre, le contrôleur répond 201 avec ce JSON. Retour dans le navigateur : React reçoit la réponse, met à jour son cache et affiche la confirmation. Le trajet complet dure quelques dizaines de millisecondes, et aucune étape ne peut assurer le rôle d'une autre.

Les variables d'environnement

L'adresse de la base n'est pas la même sur ta machine et sur le serveur de production. Le compte du prestataire de paiement non plus. Ces valeurs ne s'écrivent jamais dans le code : elles vivent dans des variables d'environnement, lues au démarrage. En développement, un fichier .env les regroupe, et ce fichier ne part pas dans Git. En production, c'est l'hébergeur qui les fournit. Les plus courantes sont l'URI de connexion MongoDB, le secret qui signe les jetons, le port d'écoute et l'adresse du front autorisée par le CORS. Attention au piège côté React : les variables préfixées pour être exposées au navigateur, NEXT_PUBLIC_ avec Next.js ou VITE_ avec Vite, finissent dans le code téléchargé par l'utilisateur. Elles servent à transmettre l'URL de l'API, surtout pas une clé secrète. Dernier réflexe utile : valider la configuration au démarrage et refuser de démarrer s'il manque une variable. Trente lignes qui évitent des pannes incompréhensibles.

Où vit chaque morceau une fois en ligne

La base tourne le plus souvent sur MongoDB Atlas, le service géré de l'éditeur, avec une liste d'adresses IP autorisées et des sauvegardes automatiques. Le back est un processus Node lancé en continu, dans un conteneur ou sur une plateforme comme Railway, Render ou Scalingo, joignable sur une adresse du genre api.monsite.com. Le front est un paquet de fichiers statiques servis par un CDN, sur www.monsite.com. Les trois sont indépendants, ce qui permet de redéployer le site sans toucher au serveur. Deux points de friction reviennent systématiquement. Le CORS d'abord : par défaut, le navigateur refuse qu'une page d'un domaine appelle une API d'un autre domaine, il faut donc autoriser explicitement le domaine du front côté serveur. Le HTTPS ensuite, obligatoire dès qu'on manipule des cookies de session.

Une arborescence qui ne part pas en vrille

Côté back, quatre couches et une règle : chacune ne connaît que la suivante. La route décrit l'adresse et les middlewares. Le contrôleur traduit le HTTP. Le service porte les règles métier. Le modèle parle à la base. On range les fichiers par fonctionnalité, commandes, facturation, authentification, plutôt que par type, pour lire une fonctionnalité entière sans ouvrir cinq dossiers. Côté front, même logique : un dossier par écran ou par domaine, les composants réutilisables à part, et les appels à l'API regroupés au même endroit au lieu d'être éparpillés dans les composants. Ce n'est pas de l'esthétique. C'est ce qui permet de revenir sur un projet six mois plus tard sans avoir à le relire en entier.

Modéliser : intégrer ou référencer

C'est la décision la plus structurante d'un projet MongoDB, et la plus pénible à corriger après coup. On intègre ce qui se lit avec le document parent et dont la taille est bornée : une adresse de livraison, les options d'un produit, les trois dernières notifications. On référence ce qui est partagé, volumineux ou sans limite connue. Le piège classique, c'est le tableau qui grossit indéfiniment. Un document plafonne à 16 Mo, il est réécrit en entier à chaque modification, et les commentaires d'un article finissent toujours par déborder. La dénormalisation, elle, reste un choix légitime : copier le nom du client dans la commande n'est pas une erreur, c'est la garantie qu'une facture émise reste juste même si le client change de nom ensuite. Et un petit champ schemaVersion dans chaque document coûte deux octets, mais sauve la migration suivante.

Les index, ou pourquoi ça marche en local et pas en prod

Sans index, MongoDB parcourt toute la collection pour trouver ce qu'on lui demande. Jusqu'à dix mille documents, personne ne le remarque. À un million, la page met huit secondes à s'afficher. Un index se pose sur les champs par lesquels on cherche, et leur ordre compte : d'abord ceux testés en égalité, ensuite ceux qui servent au tri, enfin ceux comparés par intervalle. C'est la règle ESR. Pour vérifier, explain('executionStats') donne le plan d'exécution : on veut y lire un IXSCAN et un nombre de clés examinées proche du nombre de documents renvoyés. Un COLLSCAN sur une collection qui grossit, c'est une panne programmée. Deux variantes servent tous les jours : l'index TTL, qui supprime tout seul les documents expirés, et l'index unique, seule vraie garantie qu'un email n'existe pas en double. Un contrôle écrit dans le code perd toujours la course contre deux requêtes simultanées.

Mongoose au quotidien : ce qui surprend

Quatre comportements font perdre du temps à tout le monde, autant les connaître. Une requête de lecture doit finir par lean(), sinon Mongoose fabrique un objet complet avec ses méthodes pour chaque document alors qu'on voulait juste du JSON. Les validateurs ne s'exécutent pas sur updateOne ni findOneAndUpdate tant qu'on n'a pas passé runValidators. findOneAndUpdate renvoie le document d'avant la modification, sauf si on demande new: true. Et un champ mot de passe se déclare avec select: false, pour qu'il ne parte pas dans une réponse par distraction. Reste populate, bien pratique pour remplacer un identifiant par le document lié. Ce n'est pas le N+1 qu'on décrit souvent, Mongoose regroupe les identifiants en une requête par champ, mais c'est un aller-retour de plus que la base ne peut ni trier ni filtrer. Sur une liste paginée, un champ dénormalisé fait souvent mieux.

Agréger et paginer

Quand une requête simple ne suffit plus, pour des totaux, des regroupements ou un peu de jointure, on passe à l'aggregation pipeline : une suite d'étapes où chacune reçoit ce que la précédente a produit. La règle d'or est de filtrer tôt avec $match sur des champs indexés, pour que les étapes suivantes travaillent sur le moins de documents possible. $facet est pratique aussi, il rend la page de résultats et le total en un seul aller-retour. La pagination, justement, mérite mieux que skip et limit. Demander la page 400 avec skip oblige le serveur à parcourir vingt mille documents pour n'en renvoyer aucun. La pagination par curseur trie sur un couple stable, la date de création et l'_id pour départager les égalités, et demande simplement ce qui vient après le dernier élément vu. C'est plus rapide, et ça évite les doublons quand un document est inséré pendant la lecture.

L'authentification, sans se tromper

Le mot de passe n'est jamais stocké tel quel. On garde une empreinte calculée par argon2 ou bcrypt, qu'on ne peut pas inverser. À la connexion, le serveur vérifie cette empreinte et délivre deux jetons. Un jeton d'accès à durée courte, une quinzaine de minutes, que le front joint à chaque requête. Un jeton de rafraîchissement à durée longue, rangé dans un cookie httpOnly que le JavaScript de la page ne peut pas lire. Quand l'accès expire, le front demande discrètement un nouveau couple. Chaque rafraîchissement invalide le précédent : si un jeton déjà consommé revient, c'est qu'il a été volé, et on coupe toutes les sessions du compte. Un compteur de version sur l'utilisateur permet de tout révoquer d'un coup après un changement de mot de passe. Dernier point, souvent oublié : les droits se vérifient dans le service, pas seulement dans le middleware de la route.

La liste de sécurité qu'on n'improvise pas

helmet pose les en-têtes HTTP de base. Le CORS liste explicitement les domaines autorisés, et surtout pas une étoile quand on utilise des cookies. Une limite de taille sur le corps des requêtes évite qu'un envoi de 500 Mo occupe la mémoire du serveur. Un rate limit protège le formulaire de connexion contre les listes de mots de passe fuités, à condition de régler trust proxy derrière un reverse proxy, sinon toutes les requêtes semblent venir de la même adresse et la protection ne sert plus à rien. Les fichiers ne transitent pas par l'API : on signe une URL et le navigateur dépose directement sur le stockage objet. Enfin, on refuse les clés commençant par $ dans les données reçues, sans quoi un client peut injecter des opérateurs MongoDB dans une requête.

Valider les entrées à un seul endroit

Chaque route déclare ce qu'elle accepte : la forme du corps, des paramètres et de la query. J'utilise Zod, qui a un avantage décisif, le type TypeScript se déduit du schéma, donc on ne décrit jamais la même chose deux fois. Le schéma est strict, c'est-à-dire qu'il rejette les champs inconnus. Sans ça, un client curieux peut glisser un champ role dans la mise à jour de son profil, juste pour voir si ça passe. Et comme ce même schéma est importé côté React, le formulaire refuse exactement ce que l'API refuse, avec les mêmes messages. C'est le partage de code dont je parlais au début du guide, et c'est de loin le plus rentable.

Les erreurs et les logs

Une erreur qui remonte au client doit porter un code stable, du genre AUTH_INVALID_CREDENTIALS, et un statut HTTP cohérent. Tout passe par un middleware d'erreur unique, seul endroit qui décide de la réponse et du niveau de log. Les erreurs de la base se traduisent là : le E11000 de MongoDB, qui signale un doublon sur un index unique, devient un 409 nommant le champ en conflit plutôt qu'une 500 illisible. Les logs, eux, sont structurés et portent un identifiant de requête propagé de bout en bout. Sans cet identifiant, retrouver les cinq lignes qui décrivent le même incident dans un fichier de plusieurs milliers de lignes relève de l'archéologie.

React : où vit chaque état

Il existe deux natures d'état, et les confondre produit la majorité des bugs d'interface. L'état serveur, c'est ce que possède la base : la liste des commandes, le profil, le panier enregistré. Il appartient à un cache de requêtes comme TanStack Query, qui sait quand redemander, quand invalider après une modification et comment revenir en arrière si le serveur refuse. L'état client, c'est ce qui n'existe que dans l'écran en cours : un menu ouvert, une étape de formulaire, un filtre. Celui-là vit dans le composant. Recopier des données serveur dans un useState reste la première cause d'affichages périmés. Et une valeur qu'on peut déduire d'une autre se calcule pendant le rendu, pas dans un useEffect qui déclenche un second rendu pour rien.

Next.js : quand le front absorbe une partie du back

Next.js ajoute au React classique le rendu côté serveur, la génération statique et un routage par fichiers. Concrètement, la page arrive déjà écrite dans le HTML, ce qui change tout pour le référencement et pour la vitesse d'affichage. Avec l'App Router, les composants serveur lisent les données avant l'envoi de la page, donc aucune clé d'API ne se retrouve dans le navigateur. Une question légitime se pose alors : Express est-il encore utile ? Si le site Next est le seul client, ses Route Handlers suffisent largement. Dès qu'il y a une application mobile, un back-office, des webhooks de paiement ou du temps réel, une API Express séparée reprend tout son sens, parce que les règles métier doivent vivre à un seul endroit, indépendant de la façon dont on affiche les pages.

Tester, déployer, surveiller

Les tests unitaires visent les services, là où sont les règles, et tournent sans base de données. Les tests d'intégration montent l'application avec supertest et une instance MongoDB en mémoire : ils vérifient les vraies requêtes, les vrais index et surtout les vraies autorisations. Un test qui prouve qu'un utilisateur ne peut pas lire la commande d'un autre vaut dix tests d'affichage. Pour le déploiement, une image Docker multi-étapes lancée par un utilisateur non root, des migrations d'index versionnées comme le code, et un arrêt propre sur SIGTERM pour ne pas couper les requêtes en vol à chaque mise en ligne. Une fois en production, trois indicateurs suffisent pour commencer : le temps de réponse au 95e centile, le taux d'erreurs 500 et la longueur des files de traitement. Et une sauvegarde n'existe vraiment que le jour où on a testé sa restauration.

Performance : trois leviers, dans cet ordre

D'abord les requêtes. Un index manquant explique la quasi-totalité des lenteurs, et explain le dit en trente secondes. Ensuite la charge utile : renvoyer trois cents documents complets pour en afficher dix coûte plus cher que n'importe quelle optimisation de code. On projette les champs utiles, on pagine, on compresse. Enfin le cache, avec Redis pour les lectures fréquentes et les en-têtes HTTP pour les ressources publiques, en décidant de l'invalidation en même temps que du cache et non trois mois plus tard. Node lui-même est rarement le goulot d'étranglement. C'est presque toujours la base, ou le réseau.

Quand MERN n'est pas le bon choix

Un logiciel de comptabilité, un moteur de facturation, un outil qui croise dix entités dans chaque rapport : PostgreSQL et son modèle relationnel seront plus simples et plus sûrs. MongoDB est excellent quand le document ressemble à ce qu'on lit et écrit d'un bloc, une commande, un dossier client, une configuration, et quand la forme des données bouge souvent. Enchaîner les $lookup pour simuler des jointures est le symptôme qu'on a choisi la mauvaise base. Savoir le dire à temps fait partie du métier.

Ce que cinq ans de projets MERN m'ont appris

Les projets MERN échouent rarement à cause de la technologie. Ils échouent parce que la modélisation a été expédiée au premier sprint, parce que personne n'a regardé les index avant la mise en ligne, parce que la logique métier s'est éparpillée dans les contrôleurs, ou parce que l'authentification a été bricolée un vendredi soir. Le reste se change en une journée : la librairie de state, l'hébergeur, le gestionnaire de paquets. Ce qui ne se change pas facilement, c'est un schéma déjà rempli de données et une API dont personne ne connaît le contrat. C'est là qu'il faut mettre le sérieux, et c'est exactement ce qui sépare une application qui tient trois ans d'un prototype à réécrire au bout de six mois.

19 min de lecture
Enzo Ruffin