Saltar al contenido principal
10Módulos

Patrones y Buenas Prácticas

Clase 4 de 412 min de lectura

Saber escribir export e import es una cosa. Decidir qué va en cada archivo para que el proyecto no se convierta en un cajón de sastre es otra muy distinta, y es lo que separa un proyecto cómodo de uno que da pereza abrir.

Estructura de un proyecto con módulos#

Un proyecto bien organizado separa el código por responsabilidad: cada carpeta agrupa un tipo de cosa y cada archivo hace solo una.

mi-proyecto/├── index.html├── main.js              ← Punto de entrada├── utils/│   ├── index.js         ← Barrel file (re-exporta todo)│   ├── formatear.js│   ├── validar.js│   └── fecha.js├── servicios/│   ├── index.js│   ├── api.js│   └── auth.js└── componentes/    ├── index.js    ├── header.js    └── footer.js

Fíjate en que cada carpeta tiene un index.js que re-exporta lo que hay en ella. Ese patrón tiene nombre: barrel file.

Barrel Files (archivos barril)#

Un barrel file es un archivo index.js que agrupa y re-exporta las exportaciones de los módulos de una carpeta. Pongamos que tienes estos dos archivos de utilidades:

// utils/formatear.jsexport function formatearFecha(fecha) {  return fecha.toLocaleDateString('es-ES')}export function formatearMoneda(cantidad) {  return cantidad.toFixed(2) + ' €'}
// utils/validar.jsexport function esEmailValido(email) {  return email.includes('@') && email.includes('.')}export function esNumeroPositivo(n) {  return typeof n === 'number' && n > 0}

El barrel file los reúne con la sintaxis de re-exportación que ya conoces:

// utils/index.js — El barrel fileexport { formatearFecha, formatearMoneda } from './formatear.js'export { esEmailValido, esNumeroPositivo } from './validar.js'

Y así, desde fuera, se importa todo desde un solo punto:

// main.js — Sin barrel file (verboso)import { formatearFecha } from './utils/formatear.js'import { esEmailValido } from './utils/validar.js'// main.js — Con barrel file (limpio)import { formatearFecha, esEmailValido } from './utils/index.js'

Ventajas de los barrel files

  • Imports más limpios: una sola ruta en lugar de varias.
  • Refactorización fácil: si mueves un archivo dentro de la carpeta, solo cambias el barrel file.
  • API pública clara: decides qué es público y qué es interno.

Cuándo NO usar barrel files

  • Si la carpeta tiene un solo archivo: no hay nada que agrupar.
  • Si re-exportas cientos de cosas: puedes provocar que se cargue más código del necesario.

Cuidado: los peligros de los barrel files

Suena todo muy bien, ¿verdad? Pues los barrel files son populares, pero en proyectos reales pueden dar problemas serios. Conviene conocerlos antes de llenar el proyecto de index.js.

1. Destrozan el tree shaking

Al importar una sola función desde un barrel file, el bundler puede acabar incluyendo todo lo que ese archivo re-exporta:

// Solo necesitas formatearFecha, pero...import { formatearFecha } from './utils/index.js'// ...el bundler puede cargar TAMBIÉN validar.js,// calcular.js y todo lo demás que re-exporte index.js

Resultado: tu aplicación pesa más de la cuenta. Bundlers como Webpack han mejorado, pero no siempre consiguen optimizarlo, sobre todo si los módulos tienen efectos secundarios (código que se ejecuta al importar).

2. Tiempos de compilación más lentos

En proyectos grandes, el bundler tiene que procesar todos los módulos de una carpeta cada vez que alguien importa de su barrel. Y si hay barrels anidados (uno que importa de otro), el problema se multiplica:

// ❌ Barrel files anidados: cada import dispara una cadena// components/index.js → re-exporta de 50 componentes// pages/index.js → importa de components/index.js// app.js → importa de pages/index.js// Resultado: el bundler procesa TODOS los archivos aunque solo uses uno

3. Facilitan las dependencias circulares

Con todas las exportaciones concentradas en un punto, es más fácil crear un ciclo sin darte cuenta:

// utils/index.js re-exporta formatear.js y calcular.js// Si calcular.js importa de utils/index.js para usar formatear...// ¡Dependencia circular! calcular.js → index.js → calcular.js

4. Dificultan encontrar el origen del código

Cuando todo llega desde ./utils, pierdes la pista de dónde vive realmente cada función. En un proyecto pequeño da igual; en uno con decenas de archivos, navegar por el código se complica.

¿Cuál es la alternativa? Muchas veces, la más simple: importar directamente del archivo que contiene la función.

// En lugar de pasar por el barrel file:import { formatearFecha } from './utils/index.js'// Importa directamente del archivo origen:import { formatearFecha } from './utils/formatear.js'

Es algo más verboso, pero el código compila más rápido, pesa menos y es más fácil de rastrear.

Un módulo, una responsabilidad#

Si para describir un archivo necesitas la palabra «y» varias veces, ese archivo hace demasiadas cosas. Divídelo.

// ❌ Mal: un archivo que hace de todo// mega-utils.jsexport function formatearFecha(fecha) { /* ... */ }export function validarEmail(email) { /* ... */ }export function calcularIVA(precio) { /* ... */ }export function enviarFormulario(datos) { /* ... */ }export function animarElemento(elem) { /* ... */ }

Fechas, emails, impuestos, formularios y animaciones no tienen nada que ver entre sí. Cada cosa, a su sitio:

// ✅ Bien: cada archivo con su responsabilidad// utils/formatear.js → Funciones de formato// utils/validar.js   → Funciones de validación// utils/calcular.js  → Funciones de cálculo// servicios/api.js   → Comunicación con el servidor// ui/animaciones.js  → Animaciones

Convenciones de nombrado#

Los nombres también comunican. Con estas convenciones, muy extendidas, sabes qué es cada cosa antes de abrirla.

Archivos

mi-funcion.js       ← kebab-case para archivos de utilidadesMiClase.js          ← PascalCase para archivos que exportan una clase

Exportaciones

// Funciones y variables: camelCaseexport function calcularTotal(items) { /* ... */ }export const precioBase = 9.99// Clases: PascalCaseexport class CarritoCompra { /* ... */ }// Constantes fijas: SCREAMING_SNAKE_CASEexport const MAX_INTENTOS = 3export const API_URL = 'https://api.ejemplo.com'

Patrón: módulos de configuración#

Reúne la configuración en un único módulo y haz que los demás la importen de ahí:

// config.jsexport const CONFIG = {  apiUrl: 'https://api.ejemplo.com',  maxIntentos: 3,  timeout: 5000,  idioma: 'es'}
// servicios/api.jsimport { CONFIG } from '../config.js'export async function obtenerDatos(endpoint) {  const respuesta = await fetch(CONFIG.apiUrl + endpoint)  return respuesta.json()}

Si mañana cambia la URL de la API, solo tocas config.js. Nada de buscar y reemplazar por veinte archivos.

Patrón: módulo fachada (Facade)#

Una fachada es un módulo que simplifica una API más incómoda y expone solo lo necesario. Es como el mando de la tele: tres botones claros, y la electrónica queda escondida.

Aquí envolvemos localStorage para no tener que acordarnos de JSON.stringify y JSON.parse cada vez:

// almacenamiento.js// Fachada que simplifica el uso de localStorageexport function guardar(clave, valor) {  localStorage.setItem(clave, JSON.stringify(valor))}export function leer(clave) {  const valor = localStorage.getItem(clave)  return valor ? JSON.parse(valor) : null}export function eliminar(clave) {  localStorage.removeItem(clave)}
// app.jsimport { guardar, leer } from './almacenamiento.js'guardar('usuario', { nombre: 'Ana', edad: 25 })const usuario = leer('usuario')console.log(usuario.nombre) // -> 'Ana'

El resto de la aplicación no sabe que debajo hay localStorage. Si un día lo cambias por otra tecnología, solo modificas almacenamiento.js.

Errores habituales al trabajar con módulos#

1. Dependencias circulares

Ocurren cuando el módulo A importa de B, y B importa de A. Cada uno necesita que el otro esté listo antes:

// ❌ Dependencia circular// a.jsimport { b } from './b.js'export const a = 'A usa ' + b// b.jsimport { a } from './a.js'export const b = 'B usa ' + a // 💥 ReferenceError: 'a' aún no está inicializada

La solución es romper el ciclo: mueve lo que comparten a un tercer módulo y que los dos importen de él.

// ✅ Sin dependencia circular// compartido.jsexport const valorBase = 'base'// a.jsimport { valorBase } from './compartido.js'export const a = 'A: ' + valorBase// b.jsimport { valorBase } from './compartido.js'export const b = 'B: ' + valorBase

2. Olvidar la extensión del archivo

En el navegador, la ruta de importación necesita la extensión:

Sin extensión
import { sumar } from './operaciones'
Con extensión
import { sumar } from './operaciones.js'

3. Importar sin usar

Traerte cosas «por si acaso» solo añade ruido:

// ❌ Importas algo que no usasimport { sumar, restar, multiplicar } from './operaciones.js'console.log(sumar(2, 3)) // Solo usas sumar// ✅ Importa solo lo que necesitasimport { sumar } from './operaciones.js'console.log(sumar(2, 3))

El tree shaking puede eliminar lo que no usas, pero es mejor ser explícito: quien lea el archivo sabrá de un vistazo de qué depende.

Ejemplo completo: un mini proyecto#

Juntemos las piezas en un proyecto pequeño: un módulo de configuración, un servicio que habla con la API, una utilidad de formato y un punto de entrada que lo coordina todo.

// config.jsexport const API_URL = 'https://jsonplaceholder.typicode.com'// servicios/api.jsimport { API_URL } from '../config.js'export async function obtenerUsuarios() {  const respuesta = await fetch(`${API_URL}/users`)  return respuesta.json()}export async function obtenerUsuario(id) {  const respuesta = await fetch(`${API_URL}/users/${id}`)  return respuesta.json()}// utils/formatear.jsexport function formatearNombre(usuario) {  return `${usuario.name} (${usuario.email})`}// main.jsimport { obtenerUsuarios } from './servicios/api.js'import { formatearNombre } from './utils/formatear.js'async function main() {  const usuarios = await obtenerUsuarios()  usuarios    .map(formatearNombre)    .forEach(nombre => console.log(nombre))}main()

Cada archivo tiene una sola responsabilidad, las dependencias están a la vista en los import y main.js se lee casi como una lista de tareas.

En resumen

  • Un módulo, una responsabilidad: si un archivo hace de todo, divídelo.
  • Los barrel files limpian los imports, pero pueden perjudicar el tree shaking y la compilación: úsalos con moderación.
  • Centraliza la configuración en un módulo y esconde las APIs incómodas tras una fachada.
  • Evita las dependencias circulares moviendo lo compartido a un tercer módulo.
  • En el navegador, las rutas de importación llevan extensión.