Tres horas. Media hora para aprender a repartir el código en módulos, a tratar los errores y a separar el dato de su presentación, y dos horas y media blindando tu catálogo contra los datos que no esperas.
Hoy · Hoja de ruta
- 1. Aprende: Cómo se reparte el código en ficheros con
importyexport, cómo se lanza y se captura un error, y por qué el dato y su formato son cosas distintas. - 2. Haz: Separa datos, lógica y programa principal, valida todo lo que entra y añade búsqueda y presentación.
- 3. Comprueba: Tu programa falla de forma clara en vez de dar un resultado falso, y tu buscador encuentra «teclado» escribiendo «TECLA».
Antes de empezar · 5 minutos, sin apuntes
- ¿Cuántas líneas tiene ya tu fichero principal?
- ¿Qué le pasa a
valorAlmacensi un producto no tiene precio? - ¿Qué es peor: que el programa se pare, o que devuelva un número equivocado?
Se explica
25 minutos · conceptos y demostración
Tres asuntos que comparten una idea: poner fronteras. Una entre ficheros, para que cada uno responda a una pregunta. Otra en la entrada de datos, para que el resto del código pueda confiar. La tercera, entre el dato y su presentación.
Exportar e importar
// js/datos.js
export const catalogo = [ /* ... */ ];
export const IVA = 0.21;
// js/catalogo.js
import { catalogo, IVA } from "./datos.js";
export function disponibles(productos = catalogo) {
return productos.filter((p) => p.stock > 0);
}
// js/main.js
import { disponibles, valorAlmacen } from "./catalogo.js";
console.table(disponibles());
En el documento se declara un único enlace, con su tipo:
<script type="module" src="js/main.js"></script>
Con type="module" no hace falta defer: los módulos ya se ejecutan al final. Solo se enlaza el principal: los demás llegan por sus import.
La extensión se escribe, y hace falta un servidor
En el navegador, la ruta del import lleva su .js: "./catalogo.js", no "./catalogo". Además debe comenzar por ./ o por /.
Además, los módulos no funcionan abriendo el fichero con doble clic. Verás un error de CORS con el esquema file://. Hay que servir la carpeta: la extensión Live Server de VS Code, o el servidor que montarás tú mismo en la UD5.
- datos.js · qué hay
- catalogo.js · qué se puede preguntar
- main.js · qué hacemos hoy con ello
Es el mismo criterio de siempre: cada fichero responde a una pregunta. El criterio anticipa la separación en capas que verás en la UD6 y en el módulo de servidor.
Lanzar y capturar un error
function aplicarDescuento(precio, porcentaje) {
if (typeof precio !== "number" || Number.isNaN(precio)) {
throw new TypeError("El precio debe ser un número");
}
if (porcentaje < 0 || porcentaje > 100) {
throw new RangeError("El porcentaje debe estar entre 0 y 100");
}
return precio * (1 - porcentaje / 100);
}
throw detiene la función ahí mismo. Es mejor que devolver null en silencio: quien llama se entera del problema en el momento en que ocurre y no tres funciones más tarde.
try {
const catalogo = JSON.parse(textoRecibido);
console.log(catalogo.length);
} catch (error) {
console.error(`No se pudo leer el catálogo: ${error.message}`);
} finally {
console.log("Intento terminado");
}
try ejecuta, catch recibe el error si lo hay, finally se ejecuta pase lo que pase.
Un catch vacío es peor que ningún catch
Capturar un error y no hacer nada con él —ni registrarlo, ni avisar, ni reintentar— convierte un fallo ruidoso en uno invisible. El programa continúa con datos que no tiene, y el problema aparecerá más adelante, en un sitio que no tiene nada que ver.
Captura solo lo que sabes tratar. Lo que no sepas tratar, déjalo subir.
Validar en el borde
export function crearProducto(datos) {
const errores = [];
if (!datos.nombre?.trim()) errores.push("El nombre es obligatorio");
const precio = Number(datos.precio);
if (Number.isNaN(precio) || precio < 0) errores.push("Precio inválido");
const stock = Number.parseInt(datos.stock, 10);
if (!Number.isInteger(stock) || stock < 0) errores.push("Stock inválido");
if (errores.length > 0) {
return { ok: false, errores };
}
return { ok: true, producto: { nombre: datos.nombre.trim(), precio, stock } };
}
Fíjate en dos decisiones. Primero, se recogen todos los errores y no solo el primero: en la UD4 querrás mostrarlos todos al usuario a la vez. Segundo, la función devuelve un resultado que describe qué pasó, en lugar de lanzar: para una validación esperable, un error no resulta excepcional.
Validar en el borde
Comprobar los datos en el punto donde entran al programa —el formulario, el fichero, la respuesta del servidor— y no dentro de cada función que los usa. Después de ese punto, el resto del código puede confiar.
Una búsqueda que no falla por tonterías
export function buscar(productos, consulta) {
const termino = consulta.trim().toLowerCase();
if (termino === "") return productos;
return productos.filter((p) =>
p.nombre.toLowerCase().includes(termino) ||
p.descripcion.toLowerCase().includes(termino)
);
}
Tres decisiones que evitan tres quejas: se recortan los espacios, se compara todo en minúsculas, y una consulta vacía devuelve todo en lugar de nada. Ninguno de los métodos de texto modifica el original: los textos son inmutables, y todos devuelven uno nuevo.
Fechas y formato local
const ahora = new Date();
ahora.getFullYear();
ahora.getMonth(); // ¡de 0 a 11! enero es 0
ahora.getDate(); // el día del mes
const alta = new Date("2026-09-04");
const dias = (ahora - alta) / (1000 * 60 * 60 * 24);
Que los meses empiecen en cero es la trampa histórica de las fechas en JavaScript. Restar dos fechas, por su parte, produce milisegundos y no días: el resultado debe dividirse.
const precio = 1234.5;
precio.toLocaleString("es-ES", { style: "currency", currency: "EUR" });
// "1.234,50 €"
new Date().toLocaleDateString("es-ES", {
day: "numeric", month: "long", year: "numeric"
});
// "4 de septiembre de 2026"
Guarda el dato, formatea al presentar
En tu catálogo, el precio es 89.9, un número. "89,90 €" es su presentación, y se genera en el momento de mostrarlo.
Si guardas el texto formateado, no podrás sumar, ordenar ni comparar sin deshacerlo, y el día que cambies de moneda o de idioma habrá que tocar los datos. Es la misma separación entre contenido y presentación que aprendiste en la UD1 y la UD2, aplicada a los valores.
Se trabaja
150 minutos · práctica sobre tu propio proyecto
El primer paso reorganiza el proyecto entero; los tres siguientes lo protegen. El último dibuja lo que has construido para comprobar que no se ha enredado.
Paso 1 · Tres módulos · 35 min
- Separa tu código en
datos.js,catalogo.jsymain.js. - Exporta solo lo que se use fuera; el resto, privado del módulo.
- Añade
formato.jscon las funciones de presentación de texto. - Enlaza únicamente
main.jscontype="module". - Sirve la carpeta con un servidor local y comprueba que funciona.
- Provoca a propósito un import con la extensión olvidada y lee el error.
Consultar · exportación por defecto
export default function formatearPrecio(valor) {
return `${valor.toFixed(2)} €`;
}
import formatearPrecio from "./formato.js";
Un módulo puede tener una exportación por defecto y muchas con nombre. En este módulo preferimos las nombradas: el nombre viaja con la función y no se puede renombrar sin querer.
Antes de continuar: el documento enlaza un solo script, la página se sirve por HTTP y la consola no muestra ningún error de módulo.
Paso 2 · Blinda tu catálogo · 40 min
Es el trabajo central de la sesión.
- Añade validación a
crearProductocon al menos cinco reglas. - Haz que devuelva la lista completa de errores, no solo el primero.
- Protege
valorAlmacenfrente a productos sin precio o sin stock. - Envuelve una lectura de JSON en
try/catchy da un mensaje útil, que diga qué se esperaba y qué llegó. - Distingue con
throwlos fallos de programación —un argumento del tipo equivocado— de los de datos, que devuelven un resultado con sus errores.
El apartado 5 es la decisión de diseño de la sesión: un dato inválido que escribe una persona es algo previsible y se informa; una función llamada con un array donde esperaba un número es un fallo tuyo y debe romperse ruidosamente.
Paso 3 · Cinco entradas que no deberían pasar · 20 min
Escribe cinco entradas inválidas y comprueba que ninguna atraviesa la validación:
| Entrada | Por qué es inválida | ¿La detecta tu validación? | Qué mensaje da |
|---|---|---|---|
| Nombre solo con espacios | |||
| Precio negativo | |||
| Stock con decimales | |||
| Precio como texto con coma | |||
| Campo ausente por completo |
Después prueba una entrada válida y comprueba que pasa limpia. Una validación que rechaza todo deja de ser una validación y se convierte en un muro.
Paso 4 · Búsqueda y presentación · 35 min
En js/formato.js y js/catalogo.js:
buscar(productos, consulta)insensible a mayúsculas y espacios.formatearPrecio(valor)con formato español.formatearFecha(fecha)en formato largo.- Añade a cada producto una fecha de alta y ordénalos por ella.
- Comprueba la búsqueda con seis consultas, incluidas la vacía, una con acentos y una que no encuentre nada.
Comprueba también que en datos.js los precios siguen siendo números y las fechas, fechas. Si alguno se ha convertido en texto formateado, la separación se te ha escapado.
Consultar · los métodos de texto que usarás
const texto = " Teclado Mecánico RGB ";
texto.trim() // "Teclado Mecánico RGB"
texto.toLowerCase() // " teclado mecánico rgb "
texto.includes("Mecánico") // true
texto.trim().startsWith("Tec") // true
texto.replaceAll(" ", "-")
texto.trim().split(" ") // ["Teclado", "Mecánico", "RGB"]
"1,2,3".split(",") // ["1", "2", "3"]
texto.trim().slice(0, 7) // "Teclado"
Paso 5 · El grafo de dependencias · 20 min
Dibuja en papel qué módulo importa a cuál, con una flecha por cada import.
- ¿Hay algún ciclo? Es decir, ¿algún módulo del que se pueda salir y volver siguiendo las flechas. Si lo hay, es un defecto: significa que dos ficheros se necesitan mutuamente y ninguno de los dos se puede entender por separado.
- ¿Hay algún módulo del que dependa todo? Anótalo: es el que no podrás cambiar sin revisar el resto.
- ¿Hay algún módulo que no importe nadie? O sobra, o es el principal.
- Comprueba que
datos.jsno importa acatalogo.js. Las flechas deben ir de lo general a lo concreto, no al revés.
Ese dibujo es la primera vez que ves la arquitectura de tu programa en lugar de su código. En la UD6 lo repetirás con más capas.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. Los dos retos convierten en dato lo que ahora tienes escrito como código.
Reto 1 · Las reglas como dato
Tu crearProducto tiene las reglas escritas a mano, un if por cada una. Si mañana hay que validar pedidos, clientes y facturas, habrá que escribir tres funciones más casi idénticas.
Escribe un validador genérico que reciba las reglas como dato:
const esquemaProducto = {
nombre: { tipo: "texto", obligatorio: true, minimo: 2 },
precio: { tipo: "numero", obligatorio: true, minimo: 0 },
stock: { tipo: "entero", obligatorio: true, minimo: 0 },
web: { tipo: "texto", obligatorio: false }
};
validar(datos, esquemaProducto); // { ok: false, errores: [...] }
- Escribe
validar(datos, esquema)que recorra las claves del esquema y aplique cada regla. - Que devuelva todos los errores, con el nombre del campo incluido en cada mensaje.
- Añade un tipo nuevo al esquema —por ejemplo
"fecha"— sin tocar la funciónvalidar. Si has tenido que tocarla, el diseño no era del todo genérico: arréglalo. - Escribe un segundo esquema para otra entidad de tu proyecto y comprueba que la misma función sirve.
- Explica en tres líneas qué has ganado y qué has perdido respecto a los
ifescritos a mano. Hay una pérdida real, y conviene que sepas nombrarla.
Reto 2 · La búsqueda que encuentra lo que la gente escribe
Tu búsqueda ya ignora mayúsculas y espacios. Sigue sin encontrar «teclado mecanico» si el producto se llama «Teclado Mecánico».
- Haz que ignore también los acentos. La pista: los textos se pueden normalizar con
normalize("NFD")y después eliminar las marcas diacríticas. - Comprueba estas seis consultas:
"mecanico","MECÁNICO"," tecla ","teclado rgb",""y"ñ". - La cuarta probablemente falla: busca la cadena entera y no encuentra nada porque las palabras están separadas. Haz que una consulta de varias palabras encuentre los productos que contienen todas, en cualquier orden.
- Ordena los resultados por relevancia: primero los que coinciden al principio del nombre, después los que coinciden dentro del nombre, y al final los que solo coinciden en la descripción.
- Añade un caso que tu búsqueda no deba encontrar, y compruébalo. Una búsqueda que encuentra siempre algo es tan inútil como una que no encuentra nunca nada.
Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- Tu proyecto está en módulos y se sirve por HTTP.
- Solo enlazas el módulo principal, con
type="module". - Validas los datos donde entran y devuelves todos los errores.
- Capturas solo lo que sabes tratar.
- Buscas por texto sin que la fallen mayúsculas ni espacios.
- Guardas datos y formateas solo al presentar.
Antes de cerrar · 3 minutos, sin mirar
- ¿Qué error verás si abres el fichero con doble clic?
- ¿Cuántos módulos enlazas en el documento?
- ¿Qué hace
finally? - ¿Por qué es peligroso un
catchvacío? - ¿Qué significa validar en el borde?
- ¿Por qué no se guarda el precio ya formateado?
Ver respuestas
1 · Un error de CORS por el esquema file://: los módulos exigen que la página se sirva por HTTP.
2 · Uno: el principal. Los demás entran por sus importaciones.
3 · Se ejecuta haya error o no, para lo que hay que hacer en cualquier caso.
4 · Porque oculta el fallo y el programa continúa con datos que no tiene.
5 · Comprobar los datos en el punto donde entran, para que el resto del código pueda confiar en ellos.
6 · Porque el texto formateado ya no se puede sumar ni ordenar, y ata los datos a un idioma y una moneda.
Microprueba semanal 5 · 5–10 minutos
Individual, sin IA y sin apuntes.
- Exporta una función desde un módulo e impórtala desde otro, con la ruta escrita como debe ser.
- Escribe la lectura de un JSON que puede venir roto, sin que el programa se caiga.
- ¿Qué significa validar en el borde? Pon un ejemplo de tu proyecto.