Préambule : Contrairement aux croyances, npm ne veut pas dire Node Package Manager, c’est une rétroacronymie, même si concrètement, c’est exactement ce qu’il fait 😅. Il est développé et maintenu par la société npm (site officiel), qui s’occupe aussi du registry npm publique.
Vous ne comprenez pas ce qu’il se passe lorsque vous faites un npm install ?
D’où sort le package-lock.json ?
À quoi servent toutes les commandes autres que npm install ou npm run start ?
Eh bien moi non plus je ne comprenais pas ! Mais ne vous inquiétez pas, je vais vous aider à y voir plus clair.
Dans cette série d’articles, je vous propose de faire un tour sur les différentes fonctionnalités de base de npm.
Partie 1 : Installer un projet et gérer les versions des dépendances
Démarrage d’un projet
La première chose que je fais quand je récupère un projet node, c’est de lancer la commande npm install.

Je vois plein d’informations, et au début, voilà ce que je me disais :
- Peer dependencies ??? Bah c’est juste WARN osef
- Vulnerabilities ?? Allez, on va dire que j’ai pas vu
- Un dossier
node_modules??? Si c’est un dossier ça doit être important, je commit ! package-lock.json, deux versions possibles :- Un nouveau fichier ?? Je commit pas, on va me demander ce que c’est.
- Comment ça modifié ?? J’y ai pas touché ! Allez git reset.

Décrire son projet : package.json
Pour essayer de mieux comprendre toutes ces informations et alertes, commençons par regarder le fichier qui défini notre projet, le package.json.
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react-scripts": "^4.0.3"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}
}
Un exemple issu de la génération d’un nouveau projet react
Les champs génériques
Les premières entrées name et version donnent des informations générales sur votre projet. Il en existe plein d’autres que vous pouvez retrouver sur la documentation officielle.
Scripts
Le block scripts contient une série de commandes à utiliser pour faciliter le développement dans votre projet. Chaque commande est définie par un nom et la commande à exécuter.
Pour utiliser un script, il suffit d’utiliser la commande npm run <nom du script>.
Dans tout projet de qualité, vous devriez retrouver la commande npm run test (ou le raccourci npm test).
Dépendances
Les dépendances du runtime
Le block dependencies contient l’ensemble des packages utilisés dans votre projet. Vous pouvez ajouter des dépendances via la commande npm install <package_name>.
Dans notre exemple, nous retrouvons le package react, mais vous pouvez aussi retrouver des packages de composants graphiques (antd, @mui/material, chartjs, …) ou tout autre package utilisé au runtime de votre application (lodash, axios, …).
Les dépendances de développement
Le block devDependencies contient l’ensemble des packages utilisés lors du cycle de développement de votre projet. Vous pouvez ajouter des dépendances via la commande npm install <package_name> -D.
C’est une bonne pratique de séparer les dépendances entre dependencies et devDependencies. Dans le cas d’un package, seule les dependencies seront installées et le poids du package parent en sera allégé.
Dans notre exemple, nous retrouvons les packages react-scripts et @testing-library, qui vous permettent de compiler, tester ou exécuter votre projet. Vous pouvez aussi retrouver d’autres librairies de testing (jest, mocha, chai, cypress, …), des librairies de types dans des projets en typescript (typescript, @types/node, @types/react, …).
D’autres dépendances
Il existe aussi d’autres types de dépendances, que je n’aborderais pas ici par soucis de simplicité. Ces dernières sont peu utilisées dans le développement d’application, mais peuvent servir dans le développement de packages : peerDependencies, bundledDependencies, optionalDependencies. (plus d’info dans la doc officielle).
Versioning
Concernant le versioning des dépendances, c’est la norme semver qui est de rigueur. Ce système de notation vous permet de sélectionner de façon intelligente la version du package à utiliser.
Une version s’écrit sous le format suivant : major.minor.patch
major: est incrémenté en cas debreaking change;minor: nouvelle feature, rétrocompatible;patch: bug fix, rétrocompatible.
En général, il est possible de monter de version minor et patch sans risquer un changement de comportement, et en récupérant les correctifs de sécurité et bug, ainsi que les nouvelles fonctionnalités. Un changement dans la version major indique souvent un effort supplémentaire de réécriture de votre code pour s’adapter aux changements apportés.
Il existe plusieurs façons d’écrire un ensemble de versions visées, je vous en détaille quelques-unes :
1.2.3: version figée, on souhaite avoir exactement cette version^1.2.3: première version non0figée. Ici versionmajorfigée =>1.2.3,1.3.5…~1.2.3: versionmajoretminorfigées =>1.2.3,1.2.4,1.2.5…>=1.2.3: toute version supérieure ou égale =>1.2.3,1.2.5,2.5.8latest: la dernière version
De manière générale, j’utilise principalement la notation ~1.2.3, qui me permet de bénéficier des nouveautés et des correctifs sans introduire d’incompatibilité.
Je vous invite à tester via ce petit site pour mieux appréhender les différentes notations et visualiser les versions qui correspondent à votre notation.
Plus d’info dans la doc officielle
Le fourzitout de la conf
Il est courant de voir dans le package.json des blocs spécifiques permettant de configurer les outils de développement. En fonction des outils que vous utilisez, vous avez le choix d’un fichier spécifique à la racine, de variables d’environnement ou d’un bloc dans le package.json.

Un exemple de config dans un package.json
Dans notre cas, la configuration d’eslint a été mise dans le package.json via le bloc eslintConfig, mais il était aussi possible d’avoir un fichier .eslintrc.js à la racine du projet.
Installer un projet : npm install
Maintenant que nous avons éclairci le contenu du package.json, passons à la toute première étape lorsque l’on clone un nouveau projet : npm install.
Cette commande permet de télécharger toutes les dépendances et sous-dépendances du projet, en accord avec les versions définies dans le package.json.
À chaque npm install, une résolution des versions des dépendances est calculée, les sources des dépendances sont téléchargées dans le dossier node_modules (plus de détail ici), et un fichier package-lock.json est généré/mis à jour avec l’arbre des dépendances et leur version résolue.
Une fois cette commande lancée, vous pouvez démarrer votre projet et commencer à coder !
⚠️ Si deux développeurs installent un même projet, il est possible de ne pas avoir le même arbre de dépendances (comme expliqué très brièvement dans la doc).
Il est alors important de mettre en place un mécanisme afin de toujours obtenir le même résultat après installation. Ce qui nous permettra d’éviter des bugs aléatoires non reproductibles, et d’être plus serein quant au déploiement de notre projet.
Garantir un environnement reproductible : package-lock.json
Une petite minute, on parle de résolution de version, package-lock.json, mais ça correspond à quoi tout ça ?
Pour mieux comprendre, prenons l’exemple d’un petit projet, défini par ce package.json :
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"X": "^1.0.1"
}
}
Évolution de la version dans le temps 😖
Nous avons vu qu’à chaque npm install, les versions des dépendances sont résolues. On risque donc d’avoir des versions différentes en fonction de la date d’installation du projet :
| Date | Package X |
|---|---|
| D | 1.0.2 |
| D+5 | 1.0.5 |
| D+X | 1.2.1 |
Dans le monde parfait de semver, cela ne devrait pas poser de problèmes, mais comment éviter que 2 ans plus tard, on soit incapable d’installer le projet ?
Mauvaise idée : Et si on fige la version ? 😕
J’ai longtemps eu la superbe idée de figer la version dans le package.json en utilisant "X": "1.0.2". Après un npm install, j’aurai en effet toujours la même version :
| Date | Package X |
|---|---|
| D | 1.0.2 |
| D+5 | 1.0.2 |
| D+X | 1.0.2 |
Mais j’oublie quelque chose d’important ! Et les sous-dépendances alors ?
En considérant "X": "1.0.2" et une sous-dépendance "Y": "^2.0.3" définie dans le package.json de X, après de multiples npm install, on obtient le tableau de version suivant :
| Date | Package X | Package Y |
|---|---|---|
| D | 1.0.2 | 2.0.3 |
| D+5 | 1.0.2 | 2.0.7 |
| D+X | 1.0.2 | 2.1.4 |
Bon on fait comment alors ? Je ne vais pas tout figer non plus, j’en aurais pour mille ans ! 🤨
Figer les versions avec le package-lock.json
Ce fichier permet de conserver l’arbre de résolution des dépendances et de leur version.
En considérant :
- Un
package.jsonavec"X": "^1.0.1", X dépend de"Y": "^2.0.0" - Un
package-lock.jsonqui a résolu les versions"X": "1.0.2"et"Y": "2.0.2"
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"X": "^1.0.1"
}
},
"node_modules/X": {
"name": "X",
"version": "1.0.2",
"dependencies": {
"Y": "^2.0.0"
}
},
"node_modules/Y": {
"name": "Y",
"version": "2.0.2"
}
}
}
package-lock.json généré, plus d’informations sur la structure dans la doc officielle
Je lance ma commande npm install et je me prépare à admirer la puissance de npm
| Date | Package X | Package Y |
|---|---|---|
| D | 1.0.2 | 2.0.3 |
| D+5 | 1.0.5 | 2.0.7 |
| D+X | 1.2.1 | 2.1.4 |
Quoi ?? Toutes mes versions changent ?? Mais je ne comprends plus rien ça sert à rien ce fichier !!? En plus il est modifié à chaque npm install ! 😡
La commande npm ci
Et oui, pour dire à npm d’installer les dépendances à partir du fichier package-lock.json, il faut utiliser une autre commande : npm ci.
| Date | Package X | Package Y |
|---|---|---|
| D | 1.0.2 | 2.0.3 |
| D+5 | 1.0.2 | 2.0.3 |
| D+X | 1.0.2 | 2.0.3 |
Ouf ! On a enfin figé nos versions, si on doit faire un patch dans 2 ans on ne cassera pas tout ! 🤩
Résumé de cette première partie
- On utilise
semverpour maitriser les versions de nos dépendances; npm installrésout les versions et ajoute/met à jour lepackage-lock.json;- On utilise
npm ci:- Dans l’intégration continue pour garantir une pipeline reproductible;
- En local quand on ne veut pas modifier les versions résolues.
- On commit le
package-lock.jsonpour partager la résolution de version avec ses collègues et pour l’intégration continue - On ne commit pas le
node_modules, qui est construit par les commandes d’installation
Dans les articles suivants, nous découvrirons des fonctionnalités npm un peu plus poussées, et je vous présenterai des outils pour booster votre expérience développeur sur Node !