Chapitre 222 minIntermédiaire
npm, package.json et modules ES (import, export)
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- créer un projet Node.js et lire chaque champ de son
package.json; - installer, mettre à jour et auditer des dépendances avec npm ;
- découper un programme en modules ES avec
importetexport; - relancer le serveur à chaque modification et lire un fichier
.envsans paquet.
4 exercices corrigés
npm (Node Package Manager) est le gestionnaire de paquets livré avec Node.js : il installe les bibliothèques dont ton projet dépend et lance ses scripts. Le fichier package.json est la carte d'identité du projet : son nom, ses dépendances, ses commandes. Ce chapitre crée le projet de l'API AppliFrais, explique chaque champ du package.json, la gestion des versions, l'organisation du code en modules ES avec import et export, puis deux options de Node.js qui remplacent d'anciens paquets : --watch et --env-file.
Créer le projet AppliFrais
mkdir applifrais-api
cd applifrais-api
npm init -y
npm pkg set type=module
npm init pose des questions (nom, version, description…) et écrit le package.json ; -y accepte toutes les réponses par défaut. npm pkg set type=module modifie ensuite un champ sans ouvrir l'éditeur. Voici le fichier obtenu :
{
"name": "applifrais-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module"
}
Le package.json, champ par champ
À la fin du cours, le package.json de l'API ressemble à ceci :
{
"name": "applifrais-api",
"version": "1.0.0",
"description": "API REST de gestion des frais des visiteurs médicaux GSB",
"private": true,
"type": "module",
"engines": { "node": ">=24" },
"scripts": {
"dev": "node --watch --env-file=.env src/server.js | pino-pretty",
"start": "node src/server.js",
"test": "node --env-file=.env.test --test --test-concurrency=1 \"test/**/*.test.js\""
},
"dependencies": {
"argon2": "^0.45.1",
"cors": "^2.8.6",
"express": "^5.3.0",
"express-rate-limit": "^8.7.2",
"helmet": "^8.3.0",
"jsonwebtoken": "^9.0.3",
"multer": "^2.5.0",
"mysql2": "^3.24.6",
"pino": "^10.4.0",
"pino-http": "^11.0.0",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.9.1",
"zod": "^4.6.7"
},
"devDependencies": {
"pino-pretty": "^13.2.0",
"supertest": "^7.3.1"
}
}
| Champ | Rôle |
|---|---|
name | Nom du paquet : minuscules, sans espace |
version | Version du projet, au format majeur.mineur.correctif |
private | true empêche de publier le projet par erreur sur le registre npm public |
type | "module" : les fichiers .js sont des modules ES (import/export) |
engines | Les versions de Node.js supportées ; npm avertit si la version installée ne convient pas |
scripts | Des commandes nommées, lancées avec npm run <nom> |
dependencies | Les paquets nécessaires pour faire tourner l'application |
devDependencies | Les paquets utiles seulement pour développer : tests, affichage des journaux, outils |
main | Le point d'entrée quand le projet est utilisé comme bibliothèque ; sans intérêt pour une API, on peut le retirer |
Installer des dépendances
npm install express # ajoute express aux dependencies (raccourci : npm i express)
npm install -D supertest # ajoute supertest aux devDependencies
npm install # installe tout ce que liste le package.json (après un git clone)
npm uninstall multer # retire un paquet
npm ls # liste les dépendances installées
npm install express télécharge Express et ses propres dépendances dans le dossier node_modules/ : pour Express 5.3, une soixantaine de paquets au total. Il ajoute aussi une ligne à dependencies et met à jour package-lock.json.
npx exécute la commande d'un paquet sans l'installer globalement : npx prisma generate lance la commande prisma installée dans le projet (chapitre 7), npx pino-pretty celle de pino-pretty.
Les numéros de version : semver, ^ et ~
Les paquets suivent le versionnage sémantique (semver) : MAJEUR.MINEUR.CORRECTIF.
- correctif (5.3.0 → 5.3.1) : correction de bug, rien ne change pour toi ;
- mineur (5.3 → 5.4) : nouvelles fonctions, compatibles avec l'existant ;
- majeur (4.x → 5.0) : changements incompatibles. Il faut lire le guide de migration. Le passage d'Express 4 à 5 en est un exemple (chapitre 3).
| Écrit dans package.json | Accepte | N'accepte pas |
|---|---|---|
"5.3.0" | 5.3.0 exactement | tout le reste |
"~5.3.0" | 5.3.0, 5.3.1, 5.3.9… | 5.4.0 |
"^5.3.0" | 5.3.0, 5.4.2, 5.12.0… | 6.0.0 |
"^0.45.1" | 0.45.1, 0.45.9… | 0.46.0 (avant 1.0, le chiffre du milieu joue le rôle du majeur) |
Par défaut, npm install écrit ^ : tu reçois les correctifs et les nouveautés compatibles, jamais une nouvelle version majeure. Le même principe sert à numéroter tes propres versions et tes tags Git (versions et tags).
package-lock.json et node_modules
Le package.json dit « Express 5, n'importe quelle 5.x » ; le package-lock.json note la version exacte installée de chaque paquet, y compris les dépendances des dépendances, avec une empreinte de contrôle (integrity). Il garantit que ton camarade, la CI et le serveur de production installent exactement les mêmes fichiers que toi.
| Fichier | Dans Git ? | Pourquoi |
|---|---|---|
package.json | Oui | Il décrit le projet |
package-lock.json | Oui | Il fige les versions exactes : installations reproductibles |
node_modules/ | Jamais | Des milliers de fichiers, reconstruits par npm install ; certains sont compilés pour ton système |
.env | Jamais | Il contient des secrets |
# .gitignore
node_modules/
.env
.env.test
stockage/
coverage/
Pour installer sur une machine de build ou de production, préfère npm ci à npm install : il installe exactement ce que dit le package-lock.json, échoue s'il ne correspond pas au package.json, et repart d'un node_modules vide. Avec --omit=dev, il n'installe pas les devDependencies (chapitre 14).
Mettre à jour et auditer les dépendances
npm outdated # ce qui a une version plus récente
npm update # met à jour dans les limites du package.json (^ et ~)
npm install express@5 # change explicitement de version
npm audit # cherche les vulnérabilités connues dans les dépendances
npm audit fix # applique les mises à jour compatibles qui les corrigent
Chaque dépendance est du code écrit par d'autres, qui s'exécute avec les droits de ton application. Les attaques par la chaîne d'approvisionnement visent précisément ce point : un paquet au nom presque identique à un paquet connu (typosquatting), un compte de mainteneur piraté qui publie une version piégée, un script d'installation malveillant. Les réflexes : installer peu de paquets et des paquets maintenus, vérifier le nom exact avant npm install, garder le package-lock.json dans Git, lancer npm audit dans la CI. Le sujet est développé dans le cours Sécurité des applications.
Les scripts npm
Le champ scripts donne un nom aux commandes du projet. Chaque membre de l'équipe, et la CI, lance l'application de la même façon :
npm run dev # développement : relance à chaque modification
npm start # production (raccourci de npm run start)
npm test # tests (raccourci de npm run test)
node --run dev # équivalent de npm run dev, lancé directement par Node.js, un peu plus rapide
Dans un script npm, les commandes installées dans node_modules/.bin sont disponibles sans chemin : "docs": "prisma generate" fonctionne sans npx.
Les modules ES : import et export
Un module est un fichier qui garde ses variables pour lui et choisit ce qu'il exporte. Les autres fichiers importent ce dont ils ont besoin. C'est ce qui permet de découper l'API en fichiers courts, chacun avec une responsabilité.
// src/services/montants.js
export const TAUX_KM = 0.62;
export function centimes(montant) {
return Math.round(montant * 100);
}
export default function formaterEuros(montant) {
return `${montant.toFixed(2).replace('.', ',')} €`;
}
// src/essai.js
import formaterEuros, { centimes, TAUX_KM } from './services/montants.js';
import * as montants from './services/montants.js';
console.log(formaterEuros(57 * TAUX_KM)); // 35,34 €
console.log(centimes(23.5)); // 2350
console.log(montants.TAUX_KM); // 0.62
| Forme | Export | Import |
|---|---|---|
| Export nommé | export function centimes() {} | import { centimes } from './montants.js' |
| Renommer à l'import | import { centimes as enCentimes } from './montants.js' | |
| Export par défaut (un par fichier) | export default function … | import formaterEuros from './montants.js' (nom libre) |
| Tout le module | import * as montants from './montants.js' | |
| Module intégré | import { readFile } from 'node:fs/promises' | |
| Paquet npm | import express from 'express' | |
| Fichier JSON | import tarifs from './tarifs.json' with { type: 'json' } |
Les règles qui piègent :
- pour tes propres fichiers, le chemin commence par
./ou../et l'extension.jsest obligatoire :'./montants'sans extension provoqueERR_MODULE_NOT_FOUND; - un nom sans
./('express') désigne un paquet denode_modules; - les imports sont résolus avant l'exécution : ils se placent en haut du fichier. Pour charger un module au dernier moment,
await import('./module.js')existe aussi ; - dans un module ES,
__dirnameet__filenamen'existent pas : utiliseimport.meta.dirnameetimport.meta.filename.
Ce cours préfère les exports nommés : l'import doit employer le bon nom, l'éditeur complète automatiquement, et une faute de frappe est détectée au chargement.
CommonJS : require et module.exports
Tu croiseras encore l'ancien système de modules de Node.js, CommonJS, dans des tutoriels, des projets existants et certaines bibliothèques :
// CommonJS (fichier .cjs, ou projet sans "type": "module")
const express = require('express');
const { centimes } = require('./montants');
module.exports = { formaterEuros };
| Critère | Modules ES (import) | CommonJS (require) |
|---|---|---|
| Statut | Le standard de JavaScript, aussi dans le navigateur | L'historique de Node.js |
| Activation | "type": "module" ou extension .mjs | Par défaut sans type, ou extension .cjs |
| Chargement | Analysé avant l'exécution ; await permis au niveau du module | require s'exécute à la ligne où il est écrit |
| Extension des fichiers importés | Obligatoire | Facultative |
| Dossier du fichier | import.meta.dirname | __dirname |
Les deux mondes communiquent : un module ES peut importer un paquet CommonJS (import express from 'express' fonctionne, même si Express est écrit en CommonJS), et depuis Node.js 22.12 un fichier CommonJS peut faire require() d'un module ES qui n'utilise pas await au niveau du module. Pour un nouveau projet, choisis les modules ES.
Relancer le serveur à chaque modification : node --watch
Un serveur Node.js charge ton code au démarrage. Après chaque modification, il faut l'arrêter et le relancer. L'option --watch le fait pour toi : Node.js surveille les fichiers importés et redémarre le processus dès que l'un d'eux change.
node --watch src/server.js
Elle remplace nodemon, le paquet longtemps utilisé pour ça : un paquet de moins à installer. Elle est réservée au développement : en production, on ne redémarre pas une API parce qu'un fichier change.
Les variables d'environnement et le fichier .env
Le mot de passe de la base, la clé secrète des jetons, le port : ces valeurs changent selon l'environnement (ton poste, la base de test, la production) et certaines sont secrètes. Elles ne s'écrivent donc pas dans le code. Elles passent par des variables d'environnement, que le programme lit dans process.env.
Sur ton poste, on les range dans un fichier .env à la racine du projet :
# .env : ne jamais le committer
PORT=3000
DB_HOST=localhost
DB_PORT=3306
DB_USER=applifrais_api
DB_PASSWORD=change-moi
DB_NAME=applifrais
JWT_SECRET=remplace-moi-par-une-longue-chaine-aleatoire-de-64-caracteres
CORS_ORIGIN=http://localhost:5173
Node.js lit ce fichier lui-même avec l'option --env-file, sans le paquet dotenv qu'on utilisait auparavant :
node --env-file=.env src/server.js
node --watch --env-file=.env src/server.js # le script "dev"
Ce qu'il faut savoir sur --env-file :
- toutes les valeurs sont des chaînes :
process.env.PORTvaut"3000", pas3000. Convertis avecNumber(…); - une variable déjà définie dans l'environnement garde la priorité sur le fichier :
PORT=4000 node --env-file=.env …écoute sur 4000. C'est ce qui permet à la production de tout fixer sans fichier ; - les guillemets permettent les espaces et le caractère
#dans une valeur ; un#hors guillemets commence un commentaire ; - si le fichier n'existe pas, Node.js s'arrête.
--env-file-if-exists=.envcontinue sans lui, etprocess.loadEnvFile('.env')le charge depuis le code.
Versionne à la place un .env.example : les mêmes clés avec des valeurs factices. Un nouveau développeur le copie en .env et le complète.
Pour ne pas éparpiller des process.env.XXX dans tout le code, un module config.js lit et vérifie la configuration au démarrage. Si une variable manque, l'API refuse de démarrer avec un message clair, au lieu de planter à la première requête :
// src/config.js
const obligatoires = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME', 'JWT_SECRET'];
const manquantes = obligatoires.filter((nom) => !process.env[nom]);
if (manquantes.length > 0) {
throw new Error(`Variables d'environnement manquantes : ${manquantes.join(', ')}`);
}
if (process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET doit contenir au moins 32 caractères');
}
export const config = {
env: process.env.NODE_ENV ?? 'development',
port: Number(process.env.PORT ?? 3000),
db: {
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT ?? 3306),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
jwtSecret: process.env.JWT_SECRET,
origineFront: process.env.CORS_ORIGIN ?? 'http://localhost:5173',
dossierJustificatifs: process.env.DOSSIER_JUSTIFICATIFS ?? 'stockage/justificatifs',
};
Exercices
Exercice 1 sur 4 Lire un package.json Réussi À revoir
{
"name": "gestion-prets",
"type": "module",
"scripts": { "dev": "node --watch --env-file=.env src/index.js", "test": "node --test" },
"dependencies": { "express": "^5.3.0", "mysql2": "~3.24.0" },
"devDependencies": { "supertest": "^7.3.1" }
}
- Quelle commande lance l'application en développement ? Que fait-elle ?
- Une version 3.25.0 de mysql2 sort.
npm updatel'installe-t-il ? Et une version 5.4.0 d'Express ? - Sur le serveur de production, quelle commande installe les dépendances, et supertest est-il installé ?
- Il manque un fichier au dépôt pour que tout le monde ait exactement les mêmes versions. Lequel ?
Voir la réponseMasquer la réponse de l'exercice 1
npm run dev(ounode --run dev). Elle lancesrc/index.jsen chargeant les variables du fichier.env, et relance le serveur à chaque modification d'un fichier importé.- Non pour mysql2 :
~3.24.0n'accepte que les correctifs 3.24.x. Oui pour Express :^5.3.0accepte toute version 5.x à partir de 5.3.0. npm ci --omit=dev: installation exacte depuis lepackage-lock.json, sans lesdevDependencies. supertest n'est donc pas installé, et c'est voulu.- Le
package-lock.json, qui fige la version exacte de chaque paquet.
Tu avais trouvé ?
Exercice 2 sur 4 Corriger les imports Réussi À revoir
Ce fichier src/routes/fiches.routes.js ne démarre pas. Trouve les quatre erreurs.
const express = require('express');
import { lister } from '../controllers/fiches.controller';
import Router from 'express';
import { readFile } from 'fs/promises';
const chemin = __dirname + '/../docs/openapi.yaml';
Voir la réponseMasquer la réponse de l'exercice 2
requiren'existe pas dans un module ES :import express from 'express';.- Il manque l'extension :
'../controllers/fiches.controller.js'. Routerest un export nommé d'Express :import { Router } from 'express';. Écrit ainsi,Routerreçoit l'export par défaut, c'est-à-dire la fonctionexpresselle-même.__dirnamen'existe pas dans un module ES :join(import.meta.dirname, '../docs/openapi.yaml'), avecjoinimporté denode:path.
Ce n'est pas une erreur, mais une bonne pratique : 'node:fs/promises' plutôt que 'fs/promises', pour montrer qu'il s'agit d'un module intégré.
Tu avais trouvé ?
Exercice 3 sur 4 Que contient process.env ? Réussi À revoir
Le fichier .env contient :
PORT=3000
DB_PASSWORD="mot de passe # avec dièse"
JWT_SECRET=abc # à changer
On lance PORT=4000 node --env-file=.env src/server.js. Que valent process.env.PORT, typeof process.env.PORT, process.env.DB_PASSWORD et process.env.JWT_SECRET ? Le module config.js du chapitre laisse-t-il démarrer l'API ?
Voir la réponseMasquer la réponse de l'exercice 3
process.env.PORTvaut"4000": la variable définie dans la ligne de commande garde la priorité sur le fichier.typeof process.env.PORTvaut"string": toujours une chaîne.process.env.DB_PASSWORDvaut"mot de passe # avec dièse": entre guillemets, le#fait partie de la valeur.process.env.JWT_SECRETvaut"abc": hors guillemets,# à changerest un commentaire.
L'API refuse de démarrer : DB_HOST, DB_USER et DB_NAME manquent, et config.js lève l'erreur « Variables d'environnement manquantes : DB_HOST, DB_USER, DB_NAME ». Le secret de 3 caractères serait refusé ensuite.
Tu avais trouvé ?
Exercice 4 sur 4 Organiser en modules Réussi À revoir
Écris un module src/services/etats.js qui exporte la liste des états d'une fiche (CR, CL, VA, MP) et une fonction libelleEtat(code) qui renvoie « En cours », « Clôturée », « Validée » ou « Mise en paiement ». Montre ensuite la ligne qui l'importe dans src/controllers/fiches.controller.js.
Voir la réponseMasquer la réponse de l'exercice 4
// src/services/etats.js
const LIBELLES = { CR: 'En cours', CL: 'Clôturée', VA: 'Validée', MP: 'Mise en paiement' };
export const ETATS = Object.keys(LIBELLES);
export function libelleEtat(code) {
return LIBELLES[code] ?? 'État inconnu';
}
// src/controllers/fiches.controller.js
import { ETATS, libelleEtat } from '../services/etats.js';
LIBELLES n'est pas exporté : il reste privé au module. ETATS vaut ['CR', 'CL', 'VA', 'MP'] : l'ordre d'insertion des clés est conservé.
Tu avais trouvé ?