Montées de version en production : Node.js, Express 5, React 19 et Next.js 16
Retour d’expérience sur trois projets en production remis à jour, du back-end jusqu’au front-end : par où commencer, ce qui casse sans prévenir et comment vérifier avant de livrer.
Entre 2024 et 2026, j’ai remis à jour trois projets en production, chacun parti d’une époque différente. Chez Ikomobi, j’ai proposé puis mené les montées de version de SRS et de Goodbizz, validées avec le lead dev, le chef de projet et le client.
D’où partaient ces trois projets ?
- SRS, une plateforme collaborative B2B, tournait sur la stack de son lancement en 2021, avec Node.js 14, Express 4, Mongoose 5, React 17 et React Router 5 ;
- Goodbizz, une DXP multi-tenants, reposait sur une base de 2022 ;
- le site d’Amae Production, livré en 2024, était resté sur Next.js 14.
Dans les trois cas, il fallait rejoindre les versions actuelles sans arrêter la production.
Pourquoi monter de version quand tout fonctionne ?
Une application qui tourne sans erreur peut rester des années sur les mêmes versions, mais celles-ci ont une fin de vie. Node.js 14 n’est plus maintenu depuis avril 2023, Node.js 16 depuis septembre 2023 et Node.js 18 depuis avril 2025 ; passé cette date, plus aucun correctif de sécurité ne sort pour ces versions.
Les librairies suivent le même mouvement, et certains correctifs n’existent que dans une nouvelle version majeure. jsonwebtoken 9 refuse par défaut les tokens non signés et les clés RSA de moins de 2048 bits, tandis que multer 2 corrige plusieurs failles de déni de service publiées en 2025 sur l’upload de fichiers. Côté Next.js, la politique de support ne maintient une version majeure que pendant deux ans après sa sortie, si bien que la branche 14 n’est plus supportée.
Rester en arrière finit aussi par bloquer les évolutions, dès qu’une librairie utile exige une version plus récente de React ou de Node. Et plus l’écart grandit, plus la montée coûte cher, car plusieurs versions majeures s’accumulent sur la même brique.
Sur SRS, ce travail a pris place dans le temps de TMA (tierce maintenance applicative), selon une organisation que j’ai proposée et que le chef de projet et le client ont validée. Je la décris dans l’article sur la modernisation de la plateforme.
Dans quel ordre monter les versions ?
Tout part de Node.js, car les versions actuelles des librairies fixent un minimum. Express 5 demande Node 18, React Router 7 et node-cron 4 demandent Node 20, Next.js 16 Node 20.9. Tant que les serveurs tournent sur une version plus ancienne, ces montées ne sont même pas possibles.
Sur SRS comme sur Goodbizz, j’ai donc suivi le même ordre : Node.js d’abord, puis les API, puis les fronts. Cet ordre a un avantage pratique. Quand vient le tour des interfaces, l’API qu’elles appellent a déjà changé de version et a passé sa recette.
Pour SRS, l’écart à combler était large :
- Node.js 14 → 22 pour l’API ;
- Express 4 → 5 et Mongoose 5 → 8 côté serveur, avec jsonwebtoken, multer, node-cron et nodemailer ;
- React 17 → 19 et React Router 5 → 7 pour le back-office et la PWA ;
- les librairies qui accompagnent ces deux fronts, de MUI et CKEditor à i18next, Workbox, Yup, react-datepicker ou msal-react.
Qu’est-ce qui change vraiment avec Express 5 ?
Express 5 est sorti en septembre 2024, plus de dix ans après Express 4. Son guide de migration liste d’abord des méthodes supprimées, comme req.param(), res.sendfile() ou app.del(), ainsi que des signatures abandonnées comme res.send(status) ou res.json(obj, status). Elles se retrouvent par une recherche dans le code, et le guide propose un codemod (npx codemod@latest @expressjs/v5-migration-recipe) pour ces changements mécaniques.
req.query, la rupture qui ne se voit pas
Le changement le plus délicat, que le codemod ne traite pas, touche req.query, l’objet qui contient les paramètres de l’URL. Avec Express 5, c’est un getter recalculé à partir de l’URL à chaque lecture. Une ligne comme req.query.lang = "fr" ne lève donc aucune erreur, mais la valeur a disparu à la lecture suivante.
Le parseur par défaut change aussi. Express 4 utilisait le mode « extended », qui transforme filtre[statut]=actif en objet imbriqué, alors qu’Express 5 passe au mode « simple », qui laisse la clé telle quelle. Un filtre envoyé sous forme d’objet n’arrive plus au contrôleur sous la forme attendue.
Sur l’API de SRS, des contrôleurs complétaient req.query avant de le transmettre aux services. J’ai repéré ce point en lisant le guide, avant de lancer la montée de version. La solution que j’ai mise en place rétablit le parseur qs, qui lit les paramètres imbriqués comme sous Express 4, et redéfinit req.query comme une propriété modifiable dans un middleware placé en tête de l’application :
// Simplified example
const qs = require("qs");
app.set("query parser", (str) => qs.parse(str));
app.use((req, res, next) => {
Object.defineProperty(req, "query", {
...Object.getOwnPropertyDescriptor(req, "query"),
value: req.query,
writable: true,
});
next();
});Ce middleware conserve le comportement qu’attendent les contrôleurs existants sans les reprendre un par un. À plus long terme, ne plus écrire dans req.query et transmettre ces valeurs autrement rend le code indépendant de ce contournement.
Les routes et le corps des requêtes
Express 5 s’appuie sur une nouvelle version de path-to-regexp, plus stricte sur l’écriture des chemins :
- un joker doit être nommé (/*splat au lieu de /*), comme pour la route de Goodbizz qui sert les fichiers .well-known des applications mobiles ;
- le point d’interrogation des paramètres optionnels laisse la place aux accolades (/:file{.:ext}) ;
- les expressions régulières ne sont plus acceptées dans les chemins de route.
Côté requêtes, req.body vaut désormais undefined quand aucun parseur n’a traité la requête, là où Express 4 renvoyait un objet vide, et express.urlencoded() passe à extended: false par défaut. En contrepartie, une promesse rejetée dans un handler async est enfin transmise au gestionnaire d’erreurs, sans try/catch ni next(err) à écrire à la main.
Que faut-il reprendre côté MongoDB avec Mongoose 8 ?
Mongoose est la librairie qui fait le lien entre Node.js et MongoDB. Entre la version 5 et la 8, trois versions majeures se sont accumulées, chacune avec ses ruptures. La plus visible arrive avec Mongoose 7, qui supprime les callbacks ; des méthodes comme find(), save() ou exec() ne renvoient plus que des promesses. Sur SRS, tous les accès à MongoDB sont donc passés en async/await.
Les autres changements portent sur des méthodes supprimées et des valeurs par défaut :
- remove() et update() disparaissent avec Mongoose 7, au profit de deleteOne(), deleteMany() et updateOne() ;
- count(), findOneAndRemove() et findByIdAndRemove() disparaissent avec Mongoose 8, remplacés par countDocuments(), findOneAndDelete() et findByIdAndDelete() ;
- strictQuery, l’option qui décide si un filtre portant sur un champ absent du schéma est ignoré, s’active par défaut avec Mongoose 6 puis se désactive de nouveau avec Mongoose 7.
Ce dernier point se voit mal à la relecture, puisqu’une même requête peut renvoyer d’autres documents selon la version quand son filtre contient un champ inconnu du schéma. Il se vérifie en reprenant les filtres construits à partir des paramètres reçus par l’API.
Comment passer un front React de la version 17 à la 19 ?
Le back-office et la PWA de SRS sont deux applications React de 2021. La difficulté venait autant de ce qui gravite autour de React que de React lui-même.
Les librairies d’abord
Une librairie de composants s’appuie sur des API de React qui changent d’une version à l’autre. Pour s’installer sans conflit de dépendances, chacune doit être dans une version qui déclare React 19 dans ses peerDependencies, ce qui impose parfois de sauter plusieurs versions majeures :
- react-datepicker ne l’accepte que depuis sa version 7.6, publiée en janvier 2025 ;
- @azure/msal-react, qui gère la connexion avec un compte Microsoft, l’accepte depuis mai 2025, dans sa version 3 ;
- le composant React de CKEditor l’accepte à partir de sa version 9, et CKEditor lui-même a changé de mode d’installation en version 42, avec un paquet unique ckeditor5 à la place des builds prédéfinis.
Ce que React 19 ignore sans prévenir
La plupart des suppressions de React 19 provoquent une erreur visible, qu’il s’agisse de ReactDOM.render, de findDOMNode ou des string refs. Deux d’entre elles restent silencieuses. Les propTypes ne sont plus vérifiés, et les defaultProps d’un composant fonction ne sont plus appliqués, si bien qu’une valeur de repli définie ainsi disparaît sans le moindre message.
La correction passe par les valeurs par défaut des paramètres, que le guide de React 19 recommande à la place :
// Simplified example
// React 19 ignores Card.defaultProps on a function component
export default function Card({ title = "Sans titre" }) {
return <h2>{title}</h2>;
}Le guide de React 19 conseille, pour repérer ces cas avant de migrer, une étape par React 18.3, identique à la 18.2, mais qui affiche un avertissement pour chaque API supprimée en version 19.
De React Router 5 à React Router 7
React Router 6 a changé la déclaration des routes. Switch devient Routes, la prop component laisse la place à element, exact disparaît et les routes imbriquées s’affichent avec Outlet. La navigation est revue elle aussi. En v5, on pouvait créer un objet history et le passer au Router pour naviguer depuis n’importe quel endroit du code ; en v6, l’API standard ne le prévoit plus, et la navigation passe par le hook useNavigate, utilisable uniquement dans un composant.
Le passage de la v6 à la v7 demande moins de travail. La v6 propose des future flags, des options qui activent une à une les comportements de la v7. Une fois qu’ils sont tous activés, d’après le guide officiel, la montée en v7 se fait alors sans rupture, et le paquet react-router suffit à la place de react-router-dom.
La PWA avait été créée avec Create React App, que l’équipe React a déprécié en février 2025. Je l’ai passée sur une configuration Webpack propre au projet, à l’image du back-office.
Comment monter un site Next.js de la version 14 à la 16 ?
Pour Amae, j’ai visé directement Next.js 16 plutôt que la 15, car pour ce site l’écart de code entre les deux restait faible, et le changement d’outillage autour d’ESLint était le même dans les deux cas. Passer par la 15 aurait fait deux migrations pour le même résultat.
La démarche tenait en quelques étapes, écrites dans un plan avant de commencer :
- enregistrer un état de référence (HTML généré des pages, robots.txt, sitemap, avertissements ESLint existants) pour comparer après la migration ;
- passer au dernier correctif de la branche 14 ;
- lancer le codemod officiel (npx @next/codemod@canary upgrade latest), puis relire son diff fichier par fichier ;
- passer à ESLint 9 avec le codemod next-lint-to-eslint-cli, puisque la commande next lint n’existe plus dans Next.js 16 ;
- ne monter une dépendance tierce que si sa version ne fonctionne pas avec React 19, ce qui a conduit à monter framer-motion et react-slick ;
- valider sur une preview Vercel avant la mise en production.
Les critères de réussite étaient fixés dès le départ. Il fallait une vérification des types, un lint et un build sans nouvel avertissement, des pages toujours générées en statique, et un robots.txt, un sitemap et des URL canoniques identiques à la référence.
Ce que la migration a demandé
Les types de React 19 sont plus stricts. Un appel à useRef() exige désormais un argument (useRef(null)), et un RefObject peut contenir null, d’où un typage en RefObject<HTMLIFrameElement | null>.
La configuration ESLint de Next.js 16 ajoute de nouvelles règles sur les hooks. Elles ont signalé des composants qui recopiaient dans un useState, au moyen d’un useEffect, une valeur calculable directement. Dans l’en-tête, un état isHomepage synchronisé avec l’URL est ainsi devenu une simple comparaison sur usePathname().
Sass ayant déprécié @import, les feuilles de style sont passées aux modules @use. Redux avait été retiré juste avant, au profit d’un Provider React pour les messages, ce qui faisait une dépendance de moins à migrer.
Sur le front de Goodbizz, également passé en Next.js 16, l’un des changements les plus visibles dans le code venait de Next.js 15 : params, searchParams, cookies() et headers() y deviennent asynchrones et se lisent avec await.
Avec un assistant IA
Sur SRS et Goodbizz, j’ai lu les guides et repéré moi-même les usages à migrer. Pour Amae, j’ai mené la migration avec Claude Code, et c’est désormais l’approche que le guide de Next.js 16 recommande en premier : confier la partie mécanique à un agent IA, à condition qu’il s’appuie sur la documentation de la version installée.
C’est là que la configuration compte, parce qu’un modèle a été entraîné sur des versions plus anciennes de Next.js. Le dépôt d’Amae contenait une règle qui interdit de s’en remettre à la mémoire de l’assistant pour une API et impose de vérifier sur la documentation de la version visée, via Context7. Depuis Next.js 16, cette documentation est même livrée dans le paquet, et un fichier AGENTS.md indique à l’assistant de la lire. S’y ajoutent un plan validé avant de toucher au code, des vérifications après chaque étape et un hook qui bloque toute écriture git, si bien que chaque commit reste le mien.
Le temps gagné porte sur le travail mécanique (lancer les codemods, retrouver chaque useRef à corriger, relancer le build). Les choix, eux, restent à faire, qu’il s’agisse de viser la 16 plutôt que la 15, de ne monter une dépendance que si nécessaire ou de mener la recette.
Comment vérifier sans casser la production ?
Sur SRS et Goodbizz, chaque montée de version passait par l’environnement de staging, déployé automatiquement depuis la branche develop. Je vérifiais d’abord les parcours concernés, puis le chef de projet et le client faisaient leur propre recette avant la mise en production.
Le déploiement avec Capistrano conserve les versions précédentes sur le serveur, ce qui permet de revenir en arrière rapidement si un problème apparaît après une mise en production. Ce retour arrière n’a pas servi, puisqu’aucune de ces montées de version n’a provoqué d’incident en production.
Pour Amae, la preview Vercel jouait le même rôle, avec une URL de test par branche et une recette des parcours principaux, de la vidéo d’accueil à l’envoi réel du formulaire de contact. L’audit d’accessibilité qui a suivi s’est fait sur ce build Next.js 16.
Le staging et le retour arrière limitent le risque sans remplacer la lecture des guides. Une écriture perdue dans req.query ou des defaultProps ignorés peuvent passer une recette si l’écran testé ne les sollicite pas ; c’est en amont, dans le guide de migration, qu’on les repère le plus sûrement.