module.exports es el valor que un archivo de Node.js entrega a cualquier otro archivo que lo cargue con require(). Empieza siendo un objeto vacío: añádele propiedades para exportar varias cosas, o asígnale un valor nuevo (una función, una clase) para exportar una sola. exports es una variable atajo que apunta al mismo objeto, así que exports.add = ... funciona, pero exports = ... rompe el vínculo y no exporta nada.
Imprimió 5 20 3.14159 y luego [ 'add', 'multiply', 'PI' ]. Las tres líneas de math.js añadieron propiedades al mismo objeto, tanto si pasaron por module.exports como por exports, y require('./math') devolvió ese objeto.
Exportar una función, una clase o un objeto
Cuando un módulo tiene una sola cosa principal, asígnala directamente a module.exports. Así quien lo llama recibe la función en sí, no un objeto que la envuelve:
Imprimió Hello, Ada y Grace (user). Si ves TypeError: greet is not a function, quien llama usó la forma equivocada: escribe const { greet } = require(...) para una propiedad de un objeto y const greet = require(...) para un valor asignado.
module.exports vs exports: la trampa de la reasignación
Antes de ejecutar tu archivo, Node.js hace exports = module.exports. Los dos nombres apuntan a un mismo objeto, y require devuelve module.exports. Asignar un valor nuevo a exports solo cambia la variable local:
broken.js exportó {}, porque exports ahora apunta a un objeto nuevo que nadie devuelve. working.js exportó { version: 2 }.
| Escribes | ¿Se exporta? |
|---|---|
module.exports.x = 1 | Sí |
exports.x = 1 | Sí, mismo objeto |
module.exports = { x: 1 } | Sí, reemplaza el objeto |
exports = { x: 1 } | No, el valor se pierde |
La regla más sencilla: escribe siempre module.exports. Funciona en todos los casos y el código dice lo que hace.
require devuelve el mismo objeto en caché
Node.js ejecuta el código de un módulo una sola vez, la primera vez que se requiere, y guarda module.exports en caché. Cada require posterior del mismo archivo devuelve ese mismo objeto, así que el estado dentro de un módulo lo comparten todos los archivos que lo cargan:
counter.js is running apareció una sola vez, b.next() devolvió 3, y a === b es true, porque la clave de la caché es la ruta resuelta del archivo, y ./counter y ./counter.js resuelven al mismo archivo. Por eso una conexión a base de datos o un objeto de configuración creado en un módulo se comparte en toda la app.
module.exports y ES modules
Los ES modules usan export e import. Las formas se corresponden así:
// 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 ES module puede hacer import de un archivo CommonJS: su module.exports llega como import por defecto. Desde Node.js 22.12, require() también puede cargar un ES module que no tenga await de nivel superior. Para saber cuándo elegir cada uno, consulta CommonJS vs ES modules.
Errores comunes
Mezclar exports.x con module.exports = ... en un mismo archivo. La asignación reemplaza el objeto, así que todos los exports.x escritos antes se pierden. Elige un estilo por archivo.
Exportar un valor que cambia después. module.exports = { count } copia el número en el momento de la carga; cuando el módulo actualiza count, quien lo llama sigue viendo el valor antiguo. Exporta una función que lo devuelva, como hace counter.js arriba.
Olvidar el ./. require('math') busca un paquete en node_modules, no un math.js junto a tu archivo. Los archivos locales siempre empiezan por ./ o ../.
Preguntas frecuentes
¿Puedo usar module.exports y export en el mismo archivo?
No. Un archivo es CommonJS (require, module.exports) o un ES module (import, export). Node.js lo decide por la extensión (.cjs o .mjs) o por "type" en package.json.
¿Por qué require devuelve un objeto vacío?
Normalmente porque el módulo asignó exports = ... en lugar de module.exports = ..., o porque dos módulos se requieren entre sí. En un require circular, uno de ellos recibe el module.exports del otro antes de que ese archivo termine de ejecutarse.
¿Necesito la extensión .js en require?
No. require('./math') prueba ./math, luego ./math.js, ./math.json y ./math.node, y después una carpeta ./math con un index.js. En un import de ES modules la extensión es obligatoria.
¿Cómo recargo un módulo que require ya tiene en caché?
Borra su entrada con delete require.cache[require.resolve('./math')] y vuelve a requerirlo. Esto sirve sobre todo en tests y herramientas de desarrollo; el código de la aplicación no debería depender de ello.