Skip to content

Módulos ES ​

Cuando tu proyecto crece, un solo archivo app.js de 2,000 líneas se vuelve imposible de mantener. Los módulos te permiten dividir el código en archivos pequeños, cada uno con una responsabilidad, que se importan entre sí. Es la base de cómo se organiza cualquier proyecto profesional, desde una landing hasta una app en React o Node.

Concepto ​

Piensa en una cocina profesional: hay una estación de cortes, otra de salsas, otra de postres. Cada una hace lo suyo y entrega sus productos a las demás. Un módulo es una estación:

  • Exporta (export) lo que quiere compartir.
  • Importa (import) lo que necesita de otros.
  • Todo lo demás es privado: las variables de un módulo no contaminan el ámbito global.

Antes de los módulos, todos los scripts compartían el mismo ámbito global: si dos archivos declaraban una variable total, una pisaba a la otra. Con módulos, cada archivo tiene su propio ámbito.

Exportar ​

Exportaciones nombradas ​

Un archivo puede exportar varias cosas, cada una con su nombre.

js
// utils/formato.js

export const IVA = 0.16;

export function formatearMoneda(cantidad) {
  return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(cantidad);
}

export function capitalizar(texto) {
  return texto.charAt(0).toUpperCase() + texto.slice(1);
}

// Función privada: no se exporta, nadie fuera de este archivo puede usarla
function redondear(n) {
  return Math.round(n * 100) / 100;
}

export const conIva = (precio) => redondear(precio * (1 + IVA));

También puedes exportar todo al final, lo que deja ver de un vistazo la "interfaz pública" del módulo:

js
const IVA = 0.16;
function formatearMoneda(cantidad) { /* ... */ }
function capitalizar(texto) { /* ... */ }

export { IVA, formatearMoneda, capitalizar };

Exportación por defecto ​

Cada archivo puede tener una exportación default, normalmente lo "principal" del módulo (una clase, un componente):

js
// services/Carrito.js
export default class Carrito {
  constructor() {
    this.items = [];
  }
  agregar(producto) {
    this.items.push(producto);
  }
}

Importar ​

js
// main.js

// Nombradas: entre llaves y con el MISMO nombre
import { formatearMoneda, conIva } from './utils/formato.js';

// Default: sin llaves y con el nombre que tú quieras
import Carrito from './services/Carrito.js';

// Ambas a la vez
import Carrito2, { IVA } from './services/Carrito.js';

// Renombrar (alias) para evitar choques de nombres
import { formatearMoneda as moneda } from './utils/formato.js';

// Todo el módulo como un objeto
import * as formato from './utils/formato.js';
formato.capitalizar('hola');
formato.IVA;

// Sólo ejecutar el módulo (efectos secundarios, como registrar algo)
import './analytics.js';

Error común: la extensión .js

En el navegador debes escribir la ruta completa con extensión: './utils/formato.js'. Sin .js el navegador no encuentra el archivo. Herramientas como Vite permiten omitirla, pero en módulos nativos es obligatoria. Y las rutas relativas deben empezar con ./ o ../.

¿Nombradas o default? ​

NombradasDefault
Cantidad por archivoMuchasUna
Nombre al importarEl mismo (o alias con as)El que quieras
Autocompletado del editorExcelenteRegular
Renombrados accidentalesImposiblesPosibles

Buena práctica

Muchos equipos prefieren sólo exportaciones nombradas: el editor autocompleta mejor, los refactors son más seguros y todos usan el mismo nombre. Usa default cuando una herramienta o framework lo pida (por ejemplo, componentes de Vue o páginas de Next.js).

Módulos en el navegador ​

Para que el navegador trate un archivo como módulo, usa type="module":

html
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Mi tienda</title>
  <script type="module" src="./js/main.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

Lo que cambia con type="module":

  • Se cargan con defer automáticamente (esperan a que el HTML esté listo).
  • Están en modo estricto siempre ('use strict').
  • Cada módulo tiene su propio ámbito: no crea variables globales.
  • Se ejecutan una sola vez, aunque los importes desde diez archivos.
  • Permiten await en el nivel superior.

Error común: abrir el HTML con doble clic

Si abres index.html directamente (file:///C:/...) verás un error de CORS: los navegadores no cargan módulos desde el sistema de archivos. Necesitas un servidor local:

  • Extensión Live Server de VS Code (clic derecho → Open with Live Server).
  • npx serve en la carpeta del proyecto.
  • O Vite (lo verás en npm y Vite).

Llamar funciones desde el HTML ​

Como los módulos no crean variables globales, esto no funciona:

html
<button onclick="agregarAlCarrito()">Agregar</button> <!-- ReferenceError -->

La solución correcta es registrar los eventos desde el módulo con addEventListener (que es, de todos modos, la buena práctica):

js
document.querySelector('#agregar').addEventListener('click', agregarAlCarrito);

Importación dinámica ​

import() como función carga un módulo bajo demanda y devuelve una promesa. Ideal para código pesado que no siempre se necesita (un editor, gráficas, un mapa):

js
const botonGrafica = document.querySelector('#ver-grafica');

botonGrafica.addEventListener('click', async () => {
  // El archivo sólo se descarga cuando el usuario lo pide
  const { dibujarGrafica } = await import('./graficas.js');
  dibujarGrafica(datosVentas);
});

// Cargar según una condición
const idioma = navigator.language.startsWith('en') ? 'en' : 'es';
const textos = await import(`./i18n/${idioma}.js`);

Esto se llama code splitting (división de código) y mejora mucho el tiempo de carga inicial.

Organización de un proyecto ​

Una estructura típica para un proyecto mediano en JavaScript puro:

txt
mi-tienda/
├── index.html
├── css/
│   └── estilos.css
└── js/
    ├── main.js              ← punto de entrada: arranca la app
    ├── config.js            ← constantes (URL de la API, IVA...)
    ├── api/
    │   └── productos.js     ← peticiones fetch
    ├── state/
    │   └── carrito.js       ← estado y lógica del carrito
    ├── ui/
    │   ├── renderCatalogo.js
    │   ├── renderCarrito.js
    │   └── toast.js
    └── utils/
        ├── formato.js
        └── storage.js
js
// js/config.js
export const API_URL = 'https://fakestoreapi.com';
export const IVA = 0.16;

// js/api/productos.js
import { API_URL } from '../config.js';

export async function obtenerProductos() {
  const r = await fetch(`${API_URL}/products`);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}

// js/state/carrito.js
import { storage } from '../utils/storage.js';

let items = storage.get('carrito', []);
const suscriptores = new Set();

export function obtenerItems() {
  return items;
}

export function agregar(producto) {
  const existente = items.find((i) => i.id === producto.id);
  items = existente
    ? items.map((i) => (i.id === producto.id ? { ...i, cantidad: i.cantidad + 1 } : i))
    : [...items, { ...producto, cantidad: 1 }];
  guardarYNotificar();
}

export function suscribir(fn) {
  suscriptores.add(fn);
  return () => suscriptores.delete(fn); // función para desuscribirse
}

function guardarYNotificar() {
  storage.set('carrito', items);
  suscriptores.forEach((fn) => fn(items));
}

// js/main.js
import { obtenerProductos } from './api/productos.js';
import { agregar, suscribir, obtenerItems } from './state/carrito.js';
import { renderCatalogo } from './ui/renderCatalogo.js';
import { renderCarrito } from './ui/renderCarrito.js';

const productos = await obtenerProductos();
renderCatalogo(productos, { alAgregar: agregar });
suscribir(renderCarrito);
renderCarrito(obtenerItems());

Archivos "barril" (index.js) ​

Un archivo index.js que reexporta todo lo de una carpeta simplifica los imports:

js
// js/utils/index.js
export * from './formato.js';
export * from './storage.js';
export { default as debounce } from './debounce.js';

// En otro archivo, un solo import:
import { formatearMoneda, storage, debounce } from './utils/index.js';

Buena práctica

  • Un módulo, una responsabilidad. Si no puedes describir un archivo en una frase, divídelo.
  • Evita dependencias circulares (A importa de B y B importa de A): suelen indicar que hay que extraer un tercer módulo.
  • Nombra los archivos por lo que hacen: formato.js, storage.js, no cosas.js ni helpers2.js.

Módulos ES vs. CommonJS ​

En Node.js (y en código antiguo) verás otra sintaxis llamada CommonJS:

js
// CommonJS (Node clásico)
const fs = require('fs');
module.exports = { sumar };

// Módulos ES (estándar moderno: navegador y Node actual)
import fs from 'node:fs';
export { sumar };

Hoy los módulos ES son el estándar en ambos lados. En Node se activan con "type": "module" en package.json o usando la extensión .mjs.

Ejercicios ​

  1. Crea un módulo matematicas.js que exporte sumar, restar y una constante PI_APROX = 3.14, e impórtalos en main.js.
  2. Crea un módulo Contador.js con una clase exportada por defecto que tenga incrementar() y valor. Impórtala con el nombre MiContador.
  3. Tienes dos módulos que exportan una función formatear. Impórtalas en el mismo archivo sin conflicto.
  4. Carga un módulo confeti.js sólo cuando el usuario haga clic en "Celebrar".
  5. Crea un módulo toast.js con una variable privada contenedor y una función exportada mostrarToast(mensaje) que cree el contenedor la primera vez.
Ver solución
js
// 1 — matematicas.js
export const PI_APROX = 3.14;
export const sumar = (a, b) => a + b;
export const restar = (a, b) => a - b;
// main.js
import { sumar, restar, PI_APROX } from './matematicas.js';

// 2 — Contador.js
export default class Contador {
  #valor = 0;
  incrementar() { this.#valor++; }
  get valor() { return this.#valor; }
}
// main.js
import MiContador from './Contador.js';
const c = new MiContador();

// 3
import { formatear as formatearFecha } from './fechas.js';
import { formatear as formatearMoneda } from './moneda.js';

// 4
document.querySelector('#celebrar').addEventListener('click', async () => {
  const { lanzarConfeti } = await import('./confeti.js');
  lanzarConfeti();
});

// 5 — toast.js
let contenedor; // privada al módulo

export function mostrarToast(mensaje, ms = 3000) {
  if (!contenedor) {
    contenedor = document.createElement('div');
    contenedor.className = 'toasts';
    document.body.append(contenedor);
  }
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.textContent = mensaje;
  contenedor.append(toast);
  setTimeout(() => toast.remove(), ms);
}

Resumen ​

  • Un módulo es un archivo con su propio ámbito que comparte sólo lo que exporta.
  • export nombrado (muchos por archivo, se importan con llaves) y export default (uno, nombre libre).
  • En el navegador: <script type="module">, rutas con ./ y extensión .js, y un servidor local.
  • import() dinámico carga código bajo demanda (code splitting).
  • Organiza por responsabilidad: api/, state/, ui/, utils/, con un main.js de entrada.

Siguiente: POO y clases

Academia Web — aprende, copia y construye.