Dans un projet Node.js, le dossier node_modules raconte immédiatement l’état réel des dépendances, des versions et de la chaîne d’installation. Je vais expliquer à quoi il sert, ce qu’il contient, comment npm le reconstruit et quelles règles je garde pour qu’une API reste stable entre le poste local, la CI et la production.
Les points à connaître avant de toucher aux dépendances d’une API Node.js
-
node_modulescontient les paquets installés localement, pas le code métier de l’application. -
npm installreconstruit l’arbre de dépendances à partir depackage.jsonet du lockfile s’il existe. -
npm ciest la voie la plus sûre en CI et en déploiement quand on veut une installation strictement reproductible. - On versionne le manifeste et le lockfile, pas le dossier lui-même dans un dépôt classique.
- Les workspaces changent la forme du répertoire sans changer le principe de base: dépendances locales, liage propre et installations prévisibles.
À quoi sert ce dossier dans un backend Node.js
La documentation de Node.js rappelle qu’un package est un arbre de dossiers décrit par un fichier package.json, et que cet arbre s’arrête lorsqu’on rencontre un autre package.json ou un dossier nommé node_modules. En pratique, cela veut dire que le répertoire local des dépendances est le point de rencontre entre ce que votre application déclare et ce qui est réellement installé sur disque.
Dans une API, ce dossier contient souvent des briques comme un framework HTTP, un validateur, un ORM, un client de base NoSQL, des outils de test ou des utilitaires de sécurité. Je distingue toujours deux familles: les dépendances nécessaires au runtime, et celles qui servent au build, aux tests ou à la qualité de code. Cette distinction change directement ce qui doit être présent en production.
-
dependenciespour ce qui tourne avec l’application en production. -
devDependenciespour les outils de développement, de test et de build. -
optionalDependenciespour les paquets dont l’échec d’installation ne doit pas faire tomber toute l’installation.
C’est ce rôle de “miroir installé” qui explique pourquoi un backend peut fonctionner sur une machine et échouer sur une autre si le lockfile manque ou si l’arbre a été modifié à la main. Une fois cette base posée, la question suivante devient celle de la structure réelle du répertoire.

Ce qu’il y a vraiment dedans et pourquoi l’arborescence varie
On imagine souvent ce dossier comme une simple pile de paquets. En réalité, npm y construit un arbre de dépendances qui peut être aplati, imbriqué ou partiellement lié selon la stratégie d’installation. De mon point de vue, c’est précisément là que beaucoup d’équipes se trompent: elles lisent l’arborescence comme une vérité absolue alors qu’elle n’est qu’une représentation de la résolution des paquets.
npm expose plusieurs stratégies d’installation, et elles changent la forme du dossier sans changer le résultat fonctionnel attendu par le projet.
| Stratégie | Ce qu’elle fait | Quand je la trouve utile | Limite principale |
|---|---|---|---|
hoisted par défaut |
Installe les dépendances non dupliquées au niveau supérieur et remet les doublons plus bas si nécessaire. | La plupart des applications et API classiques. | Peut masquer la véritable profondeur de certaines dépendances. |
nested |
Installe en suivant davantage la structure déclarée, sans remontée des paquets. | Quand on veut une arborescence plus proche du graphe logique. | Le dossier devient vite plus profond et plus lourd. |
shallow |
Installe surtout les dépendances directes au premier niveau. | Pour garder une vue simple sur les paquets principaux. | Les dépendances transitives restent ailleurs dans l’arbre. |
linked |
Utilise un stockage interne puis des liens dans le dossier. | Cas avancés, mono-dépôts, expérimentations contrôlées. | Moins intuitif à déboguer et encore plus dépendant de l’outillage. |
Autre détail concret: les exécutables de certains paquets sont exposés via node_modules/.bin, ce qui permet de lancer des scripts de build, de lint ou de test sans installer les outils globalement. Pour une équipe backend, c’est un gain réel de reproductibilité, parce que le chemin d’exécution reste attaché au projet lui-même.
Je retiens surtout ceci: si un bug dépend d’un paquet en double, d’un lien symbolique ou d’un chemin inattendu, le réflexe n’est pas de “nettoyer au hasard”, mais de comprendre quelle stratégie d’installation a produit cette forme. Cette lecture mène naturellement à la question de la reproductibilité.
Installer de façon reproductible entre le poste local, la CI et la production
npm indique que package-lock.json décrit l’arbre exact généré par l’installation et qu’il sert à retrouver la même structure lors des installations suivantes. C’est la pièce que je veux voir versionnée, parce qu’elle réduit les écarts entre les environnements et évite les “ça marche chez moi” difficiles à diagnostiquer sur une API.
Dans la pratique, je distingue deux commandes: npm install et npm ci. La première sert à installer et, si besoin, à ajuster l’arbre en fonction des contraintes du projet. La seconde est plus stricte: elle exige un lockfile existant, vérifie la cohérence entre ce lockfile et package.json, et supprime d’abord le dossier node_modules s’il est déjà présent avant de repartir d’une base propre.
-
npm installquand je développe, ajoute une dépendance ou fais évoluer une plage de versions. -
npm ciquand je veux une installation identique à chaque exécution, surtout en CI et en déploiement. - Le lockfile quand je veux stabiliser les versions réellement résolues, pas seulement les plages autorisées.
Je fais aussi attention aux options qui changent la forme du graphe, comme certaines stratégies d’installation ou des drapeaux de compatibilité. Si un pipeline dépend d’un réglage particulier, je préfère le centraliser dans .npmrc plutôt que de compter sur la mémoire de l’équipe. Dans un backend, cette discipline vaut largement plus que quelques secondes gagnées au premier lancement.
Une fois l’installation rendue prévisible, il reste une décision simple mais importante: qu’est-ce qu’on garde dans Git, et qu’est-ce qu’on régénère systématiquement ?
Le bon compromis entre Git, CI et sécurité
Dans un dépôt classique, je ne versionne pas le dossier des dépendances lui-même. Il est volumineux, change selon l’environnement, et génère beaucoup de bruit inutile dans l’historique. Je versionne plutôt package.json et le lockfile, parce que ce duo raconte l’intention du projet et l’état exact des dépendances installées.
Il existe une exception connue: npm-shrinkwrap.json, qui est très proche du lockfile mais peut être publié. C’est utile dans certains cas de distribution stricte, notamment pour des outils de ligne de commande ou des applications packagées, mais je le déconseille pour la majorité des bibliothèques, car il verrouille trop fortement la chaîne de dépendances descendante.
Sur le plan sécurité, ce choix a aussi un impact concret. Comme le rappelle npm, npm audit s’appuie sur la présence de package.json et de package-lock.json. Autrement dit, si le projet ne garde pas une base propre et versionnée, l’analyse des vulnérabilités devient moins fiable et les correctifs plus difficiles à appliquer sereinement.
- À garder: les manifestes, le lockfile et, si nécessaire, les réglages d’installation du projet.
- À régénérer: le dossier local des paquets sur chaque machine ou à chaque pipeline.
- À surveiller: les paquets ajoutés pour le build mais inutiles à l’exécution finale.
Cette séparation est particulièrement utile en backend, où l’on veut souvent des images de déploiement petites, lisibles et faciles à auditer. Elle devient encore plus importante dès qu’on passe aux workspaces.
Ce qui change avec les workspaces et les mono-dépôts
Les workspaces simplifient le travail sur plusieurs paquets locaux à la fois. npm automatise alors le liage des packages internes pendant npm install, ce qui évite une grande partie des manipulations manuelles qu’on faisait autrefois avec npm link. Pour une équipe backend, c’est très pratique quand on partage des utilitaires, des schémas, des clients de base de données ou des contrats d’API entre plusieurs services.
Le gain est clair, mais il faut accepter un peu plus d’abstraction. L’arborescence du dossier peut devenir moins intuitive, certains paquets apparaissent sous forme de liens symboliques, et la stratégie de résolution ne correspond plus toujours à l’image mentale d’un simple dossier plat. Je conseille donc les workspaces quand on a un vrai besoin de mutualisation, pas juste pour “faire moderne”.
Dans un mono-dépôt, le réflexe utile consiste à penser en termes de frontières de packages, pas de fichiers isolés. Le bon test n’est pas “où est installé ce dossier”, mais “est-ce que chaque paquet local est résolu de façon cohérente par la chaîne d’outils, les scripts et le déploiement”. Si cette question reste floue, les gains des workspaces peuvent vite être mangés par le temps passé à déboguer des liens mal compris.
Ce point nous amène au dernier sujet, celui que je vérifie presque toujours quand une API commence à se comporter de manière instable: les erreurs récurrentes et les bons réflexes de diagnostic.
Les erreurs que je vois le plus souvent dans les projets d’API
Un module est introuvable
Le cas classique vient d’une dépendance non installée, d’un chemin d’import incorrect ou d’un décalage entre CommonJS et ECMAScript modules. Quand le champ type du package.json ne correspond pas au format réel du fichier, Node.js peut interpréter le code autrement que prévu. Je vérifie donc toujours la cohérence entre l’extension du fichier, le type du package et la manière dont l’import est écrit.
La même dépendance apparaît en plusieurs exemplaires
Ce n’est pas forcément un problème, mais cela peut en devenir un si une bibliothèque attend une seule instance partagée. Les causes sont souvent des plages de versions trop larges, un graphe de dépendances profond ou une stratégie d’installation qui favorise les doublons. Dans ce cas, je ne “force” pas le dossier à se flatter artificiellement; je corrige plutôt la contrainte de version ou la dépendance transitive qui tire l’arbre dans deux directions.
Le projet change d’aspect selon la machine
Si l’installation n’est pas strictement reproductible, un poste Linux, macOS ou Windows peut produire des résultats différents, notamment avec des dépendances optionnelles ou des outils natifs. La réponse la plus propre reste la même: verrouiller les versions, utiliser npm ci dans la CI, et supprimer le dossier local avant de repartir quand l’arbre semble corrompu.
Lire aussi : Scraper web - API vs HTML: Bâtir un pipeline fiable et légal
La sécurité passe après le confort
Je vois encore trop de projets repousser l’audit des dépendances “à plus tard”. C’est une mauvaise idée, surtout pour une API exposée. Les correctifs de sécurité vivent rarement dans le vide: ils modifient l’arbre, changent une version mineure ou obligent à revoir une chaîne de paquets. Mieux vaut intégrer ce contrôle tôt que découvrir un écart critique au moment du déploiement.
Le réflexe que je garde avant de livrer une API en production
Avant de livrer, je vérifie toujours les mêmes trois choses: le lockfile est présent et à jour, l’installation de référence passe par npm ci, et aucun paquet inutile au runtime ne traîne dans la configuration de production. Ce petit contrôle évite une grande partie des écarts invisibles qui finissent par coûter du temps en incident.
Si je devais résumer l’état d’esprit à adopter, ce serait celui-ci: le dossier local des dépendances n’est pas une source de vérité, c’est une conséquence. La vérité vit dans les manifestes, dans le lockfile et dans la discipline de l’équipe à reconstruire l’arbre de façon identique partout où l’API s’exécute. C’est cette rigueur-là qui fait la différence entre un backend fragile et un backend prévisible.