La réécriture complète est toujours annoncée pour dans un an. Un an plus tard, le legacy est toujours en production, la nouvelle version couvre la moitié du périmètre, et l'équipe maintient deux systèmes au lieu d'un. Migrer un backend Node.js sans arrêter la production se fait autrement : par tranches livrables, dans un ordre choisi, pendant que la plateforme continue de livrer à chaque sprint. Voici la méthode que nous appliquons.
Cartographier avant de toucher quoi que ce soit
On ne migre pas ce qu'on ne voit pas. Avant la première ligne modifiée, nous construisons une carte : les services, leurs versions de runtime, leurs dépendances critiques, les consommateurs de chaque endpoint, les jobs planifiés, les files de messages. À côté de la carte, un registre des risques : ce qui n'a pas de tests, ce qui n'a pas de logs exploitables, ce qui n'a pas de propriétaire identifié.
Ce que cette cartographie révèle est remarquablement constant d'une plateforme à l'autre : des endpoints que plus personne n'appelle, des endpoints appelés par un partenaire dont personne n'a le contact, un cron oublié qui écrit dans une table que deux services lisent, des variables d'environnement dont la valeur de production ne correspond à aucune documentation. Mieux vaut découvrir tout cela sur le papier qu'au milieu d'une bascule.
La carte n'a pas besoin d'être parfaite. Elle doit être suffisante pour ordonner les tranches : commencer par les services à faible risque et à bonne couverture de tests, pour roder la mécanique, et garder pour la fin ceux que tout le monde évite. L'ordre inverse, attaquer le service le plus effrayant en premier « pour se débarrasser du pire », est une erreur classique : on essuie les plâtres de la méthode sur le service qui pardonne le moins.
Node 14 vers 18 puis 22 : une tranche est un service déployé
La règle qui fait tenir toute la migration : une tranche se termine en production, pas dans une branche. Pas de branche « migration » qui vit des semaines et diverge du tronc. Un service est mis à niveau, testé, déployé par le pipeline habituel, observé, puis on passe au suivant.
Sur une marketplace cloud B2B, une vingtaine de services sont passés de Node 14 à 18 puis 22 de cette façon, pendant que la plateforme continuait de livrer à chaque sprint. Passer par 18 avant 22 n'est pas de la prudence excessive : chaque saut isole une famille de ruptures, et un service qui casse sur 18 se diagnostique bien plus vite qu'un service qui casse après un saut de quatre versions majeures.
Ce qui casse concrètement dans ces sauts :
- OpenSSL 3 à partir de Node 17 : algorithmes retirés, options TLS plus strictes, vieux certificats et vieilles librairies de signature qui ne passent plus.
- Les modules natifs : tout ce qui compile du C++ à l'installation doit être reconstruit, et certains paquets abandonnés n'ont simplement pas de version compatible. C'est là qu'on paie les dépendances choisies dix ans plus tôt.
- La résolution DNS : depuis Node 17, les résultats ne sont plus réordonnés en faveur d'IPv4, ce qui fait surgir des timeouts sur les infrastructures où IPv6 est à moitié configuré.
- Le retrait d'API dépréciées de longue date, que le code utilisait sans bruit parce que personne ne lisait les warnings en CI.
Pendant la transition, la CI teste sur les deux versions de Node. C'est ce qui permet d'avancer service par service sans imposer un ordre global : un correctif urgent peut partir sur un service encore en Node 14 sans attendre que la migration le rattrape.
AWS SDK v2 vers v3 : la migration que tout le monde sous-estime
Sur le papier, c'est un changement d'imports. En pratique, le SDK v3 change le modèle : clients modulaires, pattern commande, pagination différente, erreurs typées différemment, disparition de .promise(). Les codemods automatisent la partie mécanique ; le vrai travail est ailleurs, dans les endroits où le code dépendait du comportement implicite du v2 : retries par défaut, timeouts, marshalling DynamoDB.
// SDK v2 : client monolithique, tout le SDK charge au démarrage
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const obj = await s3.getObject({ Bucket, Key }).promise();
// SDK v3 : client modulaire, pattern commande
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
const obj = await s3.send(new GetObjectCommand({ Bucket, Key }));
Faire cohabiter v2 et v3 dans un même service pendant la transition est acceptable, à condition que ce soit un état de passage et pas une cohabitation permanente : la tranche se découpe par service, pas par ligne. Le gain n'est pas cosmétique. Les clients modulaires se prêtent au tree-shaking, et sur des fonctions Lambda, la réduction du bundle et le chargement des clients à la demande sont une part importante de ce qui a fait passer des cold starts d'environ 8 secondes à environ 1,5.
Le strangler pattern sur les endpoints legacy
Pour les services qu'une mise à niveau ne suffit pas à sauver, nous ne remplaçons pas le service entier d'un coup : nous appliquons le strangler pattern. Un point d'entrée unique devant le legacy, API Gateway ou reverse proxy, puis une migration endpoint par endpoint : la nouvelle implémentation prend une route, les réponses sont comparées sur du trafic réel, la bascule se fait, l'ancienne route est supprimée.
Deux conditions rendent le pattern honnête :
- La parité doit être observable. Logs structurés et identifiant de corrélation des deux côtés, sinon « les réponses sont identiques » est une opinion, pas un fait.
- Chaque bascule doit être réversible indépendamment. Revenir en arrière sur une route, pas sur toute la migration.
Une tranche qui ne peut pas revenir en arrière n'est pas une tranche, c'est un pari.
Et la partie que les équipes oublient : supprimer. Un strangler qui ne finit jamais d'étrangler laisse deux systèmes en production, le pire des deux mondes. Tant que l'ancienne route existe encore, la tranche n'est pas terminée.
Un backend que plus personne ne veut toucher ? Décrivez-le : diagnostic d'une page sous 48 h.
Recevoir mon diagnostic →Harmoniser les erreurs pendant qu'on y est
Les backends qui ont grandi vite répondent 500 pour tout : entrée invalide, dépendance en panne, bug, tout se ressemble. En temps normal, c'est un problème de support. Pendant une migration, c'est un problème de méthode : impossible de comparer le comportement de l'ancienne et de la nouvelle implémentation si tous les échecs sont opaques.
Nous définissons donc un contrat d'erreurs tôt dans le chantier : codes explicites, corps de réponse documenté, identifiant de corrélation. Le contrat s'applique d'abord aux endpoints legacy, un travail ingrat mais borné, puis chaque tranche migrée en hérite. À la fin, la plateforme est passée des 500 opaques à des codes explicites et documentés. Ce n'était pas l'objectif affiché de la migration ; c'en était l'échafaudage.
Pourquoi la réécriture totale annoncée pour dans un an échoue
La réécriture big-bang échoue rarement par incompétence. Elle échoue par construction :
- La spécification, c'est le code. Personne ne connaît tout ce que fait un backend qui a dix ans de production derrière lui. La réécriture redécouvre les règles métier une par une, souvent après la bascule, chez les clients.
- Le legacy est gelé, le métier ne l'est pas. Les demandes continuent d'arriver. Soit elles entrent dans le legacy et la cible court après une spécification mouvante, soit elles attendent et la réécriture devient responsable de tout ce qui ne sort pas.
- La valeur arrive à la fin, donc souvent jamais. Une migration par tranches met quelque chose en production dès la première tranche. Une réécriture ne livre rien tant qu'elle n'a pas tout livré, et le jour de la bascule concentre tout le risque accumulé en un seul événement.
Nous ne disons pas que la réécriture ne réussit jamais : sur un périmètre petit, fermé et bien connu de l'équipe, elle peut être le bon choix. Mais « on réécrit tout, ce sera prêt dans un an » sur une plateforme qui doit continuer de livrer, nous ne l'avons jamais vu tenir, et nous ne le proposons pas.
Ce qui reste difficile, même avec la méthode
La migration par tranches n'efface pas les difficultés, elle les rend gérables. Ce qui reste dur :
- La tentation du « tant qu'on y est ». Une tranche qui devait mettre à niveau un runtime se met à refactorer l'architecture, grossit, et cesse d'être livrable. Tenir le périmètre d'une tranche est une discipline, pas une évidence.
- Les consommateurs cachés. La cartographie en trouve beaucoup, jamais tous. Il en surgit après une bascule ; c'est précisément pour cela que la réversibilité par tranche n'est pas négociable.
- La fatigue de migration. Au bout de plusieurs mois, l'équipe veut passer à autre chose, et les derniers services, souvent les pires, restent. Le registre initial sert aussi à cela : savoir exactement ce qui reste, et ne pas déclarer victoire aux trois quarts du chemin.
Et deux choses que nous ne refaisons plus. Migrer le runtime, le SDK et l'architecture d'un service dans la même tranche : quand un incident survient, impossible d'attribuer la cause. Et commencer une migration sans avoir d'abord remis la supervision au niveau : on ne pilote pas une bascule à l'aveugle, et le legacy que l'on reprend est rarement bien instrumenté.
C'est ce périmètre exact que couvre notre offre de reprise et modernisation de back-end : reprendre une base de code que nous n'avons pas écrite, la cartographier, puis la moderniser par tranches pendant que la livraison continue. Nous menons ces missions à distance partout en France, avec des journées sur site pour les équipes d'Île-de-France : notre page agence web à Paris détaille cette organisation.