Módulos: CommonJS, ES Modules, fs y path
💡 🎯 Al terminar esta lección podrás…
- Distinguir CommonJS (
require) de ES Modules (import) y activar el segundo. - Exportar e importar funciones entre archivos.
- Leer y escribir archivos de texto y JSON con
fs/promises. - Construir rutas seguras con
path. - Leer argumentos con
process.argv.
1.Por qué existen los módulos
Un programa real no cabe en un solo archivo. Se divide en módulos: archivos pequeños, cada uno con una responsabilidad, que se exportan piezas (funciones, objetos, constantes) y se importan unos a otros. Así el código se ordena, se reutiliza y se prueba por partes.
En Node conviven dos sistemas de módulos, por razones históricas:
| CommonJS (CJS) | ES Modules (ESM) | |
|---|---|---|
| Sintaxis | require() y module.exports | import y export |
| Origen | El sistema original de Node (2009) | El estándar de JavaScript; el mismo que usan los navegadores y React |
| Se activa con | Es el valor por defecto de los archivos .js si no indicas lo contrario | "type": "module" en package.json, o la extensión .mjs |
| Se carga | De forma síncrona | De forma asíncrona (permite await en el nivel superior) |
⚠️ Importante · En este curso usaremos ES Modules
Es la sintaxis moderna y la misma que ya usas en React. Verás CommonJS en muchísimos proyectos y tutoriales antiguos, así que conviene reconocerlo. Lo explicamos primero para que no te sorprenda.
2.CommonJS: require y module.exports
Imagina dos archivos en la misma carpeta. En mates.js decides qué se comparte con module.exports; en app.js lo recoges con require:
function sumar(a, b) {
return a + b;
}
function multiplicar(a, b) {
return a * b;
}
module.exports = { sumar, multiplicar };
const { sumar, multiplicar } = require("./mates.js");
console.log(sumar(2, 3));
console.log(multiplicar(4, 5));
5
20
Lo que no está dentro de module.exports queda privado en su archivo. Las rutas que empiezan por ./ son relativas al archivo actual. Los nombres sin ruta (require("fs")) buscan módulos integrados o paquetes instalados.
📌 Nota · Por qué lo ves en tantos sitios
Durante más de diez años CommonJS fue la única opción. Por eso la mayoría de tutoriales antiguos y muchos proyectos existentes usan require.
3.ES Modules: import y export
Con ES Modules el mismo ejemplo se escribe con export e import. Primero se activa el modo módulo en package.json:
{
"name": "curso-node",
"version": "1.0.0",
"type": "module"
}
export function sumar(a, b) {
return a + b;
}
export function multiplicar(a, b) {
return a * b;
}
// Una exportación por defecto: solo puede haber una por archivo
export default function restar(a, b) {
return a - b;
}
import restar, { sumar, multiplicar } from "./mates.js";
console.log(sumar(2, 3));
console.log(multiplicar(4, 5));
console.log(restar(9, 4));
5
20
5
| Forma | Se exporta con | Se importa con |
|---|---|---|
| Con nombre (varias por archivo) | export function sumar() {} | import { sumar } from "./mates.js" |
| Por defecto (una por archivo) | export default restar | import restar from "./mates.js" (el nombre lo eliges tú) |
| Todo junto | — | import * as mates from "./mates.js" |
⚠️ Importante · En ESM la extensión es obligatoria
En imports relativos debes escribir la extensión completa: "./mates.js", no "./mates". Es una diferencia frecuente con lo que se hace en React con Vite, donde se puede omitir.
Diferencias que te encontrarás
- En ESM no existen
require,module,__dirnameni__filename. Para saber la carpeta del archivo actual usaimport.meta.dirnameyimport.meta.filename(disponibles en Node 20.11 y posteriores). - Puedes usar
awaitdirectamente en el nivel superior del archivo, sin envolverlo en una funciónasync. - Un archivo es CommonJS o ESM: no mezcles
requireconimporten el mismo archivo.
💡 Consejo · Cómo saber en qué modo estás
Si ves SyntaxError: Cannot use import statement outside a module, tu proyecto está en CommonJS: añade "type": "module" a package.json. Si ves require is not defined in ES module scope, es lo contrario: estás en ESM y usaste require.
4.Módulos integrados y el prefijo node:
Node incluye muchos módulos sin necesidad de instalar nada: fs (archivos), path (rutas), http (servidores), os (sistema), crypto (criptografía)… Se importan con el prefijo node:, que deja claro que son del propio Node y no paquetes de terceros:
import fs from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import os from "node:os";
Por ahora usarás dos de ellos: fs para leer y escribir archivos y path para construir rutas. En la lección 4 llegará el servidor web.
5.Leer y escribir archivos con fs
El módulo fs (file system) trabaja con archivos y carpetas. Hoy se usa su versión basada en promesas, node:fs/promises, con await. Crea un archivo notas.txt con dos líneas y este script leer.js:
Comprar pan
Estudiar Node
import { readFile } from "node:fs/promises";
const texto = await readFile("notas.txt", "utf8");
console.log(texto);
console.log("Líneas:", texto.split("\n").length);
Comprar pan
Estudiar Node
Líneas: 2
El segundo argumento, "utf8", indica que quieres texto. Sin él, readFile devuelve un Buffer (bytes sin interpretar). Escribir es igual de directo:
import { writeFile, appendFile } from "node:fs/promises";
// Crea el archivo o lo SOBRESCRIBE si ya existe
await writeFile("saludo.txt", "Hola desde Node\n", "utf8");
// Añade al final sin borrar lo anterior
await appendFile("saludo.txt", "Otra línea\n", "utf8");
console.log("Archivos escritos");
Archivos escritos
| Función | Qué hace |
|---|---|
readFile(ruta, "utf8") | Lee un archivo completo como texto |
writeFile(ruta, contenido) | Crea o sobrescribe un archivo |
appendFile(ruta, contenido) | Añade contenido al final |
readdir(ruta) | Lista los nombres de una carpeta |
mkdir(ruta, { recursive: true }) | Crea una carpeta (y las intermedias) |
rm(ruta) | Borra un archivo (con { recursive: true }, también carpetas) |
Archivos JSON: la base de datos más simple
Como JSON es texto, guardar y leer datos estructurados es combinar fs con JSON.stringify y JSON.parse:
import { readFile, writeFile } from "node:fs/promises";
const tareas = [
{ id: 1, texto: "Instalar Node", hecha: true },
{ id: 2, texto: "Aprender módulos", hecha: false }
];
// null, 2 → JSON con sangría, fácil de leer
await writeFile("tareas.json", JSON.stringify(tareas, null, 2), "utf8");
const leidas = JSON.parse(await readFile("tareas.json", "utf8"));
console.log(leidas.length, "tareas;", "primera:", leidas[0].texto);
2 tareas; primera: Instalar Node
Cuando el archivo no existe
Si pides un archivo que no existe, la promesa se rechaza con un error cuyo código es ENOENT (no such file or directory). Se atrapa con try/catch:
import { readFile } from "node:fs/promises";
try {
const texto = await readFile("no-existe.txt", "utf8");
console.log(texto);
} catch (error) {
if (error.code === "ENOENT") {
console.log("Ese archivo no existe");
} else {
throw error; // otro problema: no lo ocultes
}
}
Ese archivo no existe
⚠️ Importante · Las rutas relativas dependen de dónde ejecutas
readFile("notas.txt") busca el archivo en la carpeta desde la que lanzaste el comando, no en la carpeta del script. Si ejecutas node src/leer.js desde la raíz, buscará notas.txt en la raíz. Para anclar la ruta al script usa path.join(import.meta.dirname, "notas.txt").
6.Construir rutas con path
Las rutas se escriben distinto según el sistema: datos/notas.txt en macOS y Linux, datos\notas.txt en Windows. Concatenar textos a mano con "/" se rompe tarde o temprano. El módulo path resuelve esas diferencias:
import path from "node:path";
const ruta = path.join("datos", "2026", "notas.txt");
console.log(ruta);
console.log(path.basename(ruta));
console.log(path.basename(ruta, ".txt"));
console.log(path.extname(ruta));
console.log(path.dirname(ruta));
datos/2026/notas.txt
notas.txt
notas
.txt
datos/2026
La salida de arriba es la de macOS y Linux; en Windows las barras aparecen como \. Resumen:
| Función | Para qué sirve | Ejemplo → resultado |
|---|---|---|
path.join(...partes) | Une partes con el separador correcto | join("a", "b", "c.txt") → a/b/c.txt |
path.resolve(...partes) | Construye una ruta absoluta | resolve("datos") → /tu/carpeta/datos |
path.basename(ruta) | Nombre del archivo | basename("/a/b.txt") → b.txt |
path.extname(ruta) | Extensión (con punto) | extname("b.txt") → .txt |
path.dirname(ruta) | La carpeta que lo contiene | dirname("/a/b.txt") → /a |
7.Argumentos de la línea de comandos: process.argv
Un script puede recibir datos al ejecutarlo: node saludo.js Ana. Esos datos llegan en process.argv, un array cuyos dos primeros elementos son la ruta de node y la del script; los tuyos empiezan en la posición 2.
console.log(process.argv);
const [nombre = "Mundo"] = process.argv.slice(2);
console.log(`Hola, ${nombre}`);
node saludo.js Ana
[
'/usr/local/bin/node',
'/home/ana/curso-node/saludo.js',
'Ana'
]
Hola, Ana
Con slice(2) te quedas solo con lo que escribió la persona. Para terminar el programa indicando que hubo un problema, asigna un código de salida: 0 significa éxito y cualquier otro número, error.
const [archivo] = process.argv.slice(2);
if (!archivo) {
console.error("Uso: node validar.js <archivo>");
process.exitCode = 1; // termina con error cuando acabe lo pendiente
} else {
console.log("Procesando", archivo);
}
Ahora la parte práctica: un analizador de argumentos de estilo --clave valor es JavaScript puro y puedes probarlo aquí mismo, en el navegador.
💡 Consejo · Existe una utilidad integrada
Node incluye parseArgs en el módulo node:util, que hace este trabajo con más garantías. Aquí lo escribimos a mano para entender qué ocurre por dentro.
8.Errores frecuentes
- Olvidar la extensión en ESM:
import ... from "./mates"falla conERR_MODULE_NOT_FOUND. Escribe"./mates.js". - Mezclar
requireeimport: dentro de un archivo ESM,requireno existe. - Olvidar
"type": "module"y obtenerCannot use import statement outside a module. - Usar
__dirnameen ESM: no existe. Usaimport.meta.dirname. - Olvidar
awaital leer un archivo: obtienesPromise { <pending> }en lugar del texto. - Sobrescribir sin querer con
writeFile: si solo quieres añadir al final, usaappendFile. - Contar mal los argumentos:
process.argv[0]y[1]no son tuyos; usaslice(2).
📒 Apuntes para llevar
- Un módulo es un archivo que exporta piezas para que otros las importen.
- CommonJS:
require()ymodule.exports. ESM:importyexport. - Para usar ESM pon
"type": "module"enpackage.json(o usa.mjs). - En ESM los imports relativos llevan extensión y no existen
__dirnamenirequire. - Los módulos propios de Node se importan con el prefijo
node:(node:fs,node:path). readFile,writeFileyappendFiledefs/promisestrabajan conawait.- Usa
path.joinen lugar de concatenar rutas a mano. process.argv.slice(2)contiene los argumentos del usuario.
9.Glosario
| Término | Significado |
|---|---|
| Módulo | Archivo de código que exporta piezas para reutilizarlas desde otros archivos. |
| CommonJS | Sistema de módulos original de Node: require / module.exports. |
| ES Modules (ESM) | Sistema de módulos estándar de JavaScript: import / export. |
fs | Módulo integrado para trabajar con archivos y carpetas. |
path | Módulo integrado para construir y analizar rutas. |
| Buffer | Bloque de bytes sin interpretar; es lo que devuelve readFile si no indicas la codificación. |
ENOENT | Código de error que significa «el archivo o carpeta no existe». |
process.argv | Array con los argumentos con los que se lanzó el script. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Qué sintaxis pertenece a ES Modules?
import y export son ESM. require y module.exports son CommonJS.
¿Cómo activas ES Modules en un proyecto?
También funcionan los archivos con extensión .mjs, pero lo habitual es la opción de package.json.
En ESM, ¿cuál es la forma correcta de importar un archivo local?
En ESM la extensión es obligatoria en las rutas relativas.
¿Qué devuelve await readFile("a.txt", "utf8")?
Con "utf8" obtienes el texto. Sin la codificación obtendrías un Buffer.
Ejecutas node app.js uno dos. ¿Qué es process.argv.slice(2)?
Las posiciones 0 y 1 son la ruta de node y la del script; tus argumentos empiezan en la 2.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.