module.exports est la valeur qu'un fichier Node.js donne à tout autre fichier qui le charge avec require(). C'est au départ un objet vide : ajoutez-lui des propriétés pour exporter plusieurs choses, ou assignez-lui une nouvelle valeur (une fonction, une classe) pour en exporter une seule. exports est une variable raccourcie qui pointe vers le même objet, donc exports.add = ... fonctionne, mais exports = ... casse le lien et n'exporte rien.
Le programme a affiché 5 20 3.14159, puis [ 'add', 'multiply', 'PI' ]. Les trois lignes de math.js ont ajouté des propriétés au même objet, qu'elles passent par module.exports ou par exports, et require('./math') a renvoyé cet objet.
Exporter une fonction, une classe ou un objet
Quand un module a un seul élément principal, assignez-le directement à module.exports. L'appelant reçoit alors la fonction elle-même, pas un objet qui l'entoure :
Le programme a affiché Hello, Ada et Grace (user). Si vous voyez TypeError: greet is not a function, l'appelant a utilisé la mauvaise forme : écrivez const { greet } = require(...) pour une propriété d'objet et const greet = require(...) pour une valeur assignée.
module.exports ou exports : le piège de la réassignation
Avant que votre fichier s'exécute, Node.js fait exports = module.exports. Les deux noms pointent vers un même objet, et require renvoie module.exports. Assigner une nouvelle valeur à exports ne change que la variable locale :
broken.js a exporté {}, parce que exports pointe maintenant vers un nouvel objet que rien ne renvoie. working.js a exporté { version: 2 }.
| Vous écrivez | Exporté ? |
|---|---|
module.exports.x = 1 | Oui |
exports.x = 1 | Oui, même objet |
module.exports = { x: 1 } | Oui, remplace l'objet |
exports = { x: 1 } | Non, la valeur est perdue |
La règle la plus simple : écrivez toujours module.exports. Ça marche dans tous les cas, et le code dit ce qu'il fait.
require renvoie le même objet mis en cache
Node.js exécute le code d'un module une seule fois, la première fois qu'il est requis, et met module.exports en cache. Chaque require suivant du même fichier renvoie ce même objet, donc l'état interne d'un module est partagé par tous les fichiers qui le chargent :
counter.js is running n'est apparu qu'une fois, b.next() a renvoyé 3, et a === b vaut true, car la clé du cache est le chemin de fichier résolu, et ./counter et ./counter.js se résolvent vers le même fichier. C'est pourquoi une connexion à la base de données ou un objet de configuration créé dans un module est partagé dans toute l'application.
module.exports et les modules ES
Les modules ES utilisent export et import à la place. Les formes se correspondent ainsi :
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
Un module ES peut faire import d'un fichier CommonJS : son module.exports arrive comme import par défaut. Depuis Node.js 22.12, require() peut aussi charger un module ES qui n'a pas de await au niveau supérieur. Pour savoir lequel choisir, voir CommonJS ou modules ES.
Erreurs fréquentes
Mélanger exports.x et module.exports = ... dans un même fichier. L'assignation remplace l'objet, donc tous les exports.x écrits avant elle disparaissent. Choisissez un seul style par fichier.
Exporter une valeur qui change plus tard. module.exports = { count } copie le nombre au moment du chargement ; quand le module met count à jour, les appelants voient toujours l'ancienne valeur. Exportez une fonction qui la renvoie, comme le fait counter.js ci-dessus.
Oublier ./. require('math') cherche un package dans node_modules, pas math.js à côté de votre fichier. Les fichiers locaux commencent toujours par ./ ou ../.
Questions fréquentes
Peut-on utiliser module.exports et export dans le même fichier ?
Non. Un fichier est soit en CommonJS (require, module.exports), soit un module ES (import, export). Node.js décide selon l'extension (.cjs ou .mjs) ou selon "type" dans package.json.
Pourquoi require renvoie-t-il un objet vide ?
En général, le module a écrit exports = ... au lieu de module.exports = ..., ou deux modules se requièrent mutuellement. Dans un require circulaire, l'un des deux reçoit le module.exports de l'autre avant que ce fichier ait fini de s'exécuter.
Faut-il l'extension .js dans require ?
Non. require('./math') essaie ./math, puis ./math.js, ./math.json et ./math.node, puis un dossier ./math contenant un index.js. Dans un import de module ES, l'extension est obligatoire.
Comment recharger un module déjà mis en cache par require ?
Supprimez son entrée avec delete require.cache[require.resolve('./math')] et requérez-le à nouveau. C'est surtout utile dans les tests et les outils de dev ; le code applicatif ne devrait pas en dépendre.