Cuando ya escribes JavaScript que funciona, el siguiente nivel es escribir JavaScript que se mantiene: código limpio, organizado con patrones de diseño y respaldado con tests. Es lo que separa a quien apaña un script de quien construye software que otros pueden tocar sin miedo. En esta guía tienes lo esencial: buenas prácticas, cuatro patrones clásicos y una introducción al testing.
De cada uno de estos patrones de diseño en JavaScript verás la versión clásica y la moderna, que a menudo ya trae el propio lenguaje. He probado todo el código en Node 22, tests incluidos.
Respuesta rápida: los patrones de diseño en JavaScript son soluciones probadas a problemas que se repiten al organizar código. Los cuatro que más vas a usar son estos:
| Patrón | Qué resuelve | En JavaScript moderno |
|---|---|---|
| Singleton | Una sola instancia compartida (configuración, conexión) | Un módulo ES que exporta un objeto |
| Observer | Avisar a varios interesados cuando algo cambia | EventTarget y addEventListener |
| Factory | Crear objetos sin exponer cómo se construyen | Una función que devuelve objetos |
| Strategy | Cambiar un comportamiento sin cadenas de if | Un objeto de funciones |
Forma parte de la hoja de ruta para aprender JavaScript. Llega aquí cuando ya manejes funciones, objetos y asincronía.
Código limpio: reglas básicas
- Nombres que se entienden:
calcularTotalCarrito(), nocalc(). El código se lee muchas más veces de las que se escribe. - Funciones cortas y con una sola responsabilidad: si una función hace tres cosas, pártela en tres.
- Evita repetirte (DRY): si copias y pegas un bloque, conviértelo en una función reutilizable.
- Funciones puras cuando puedas: mismas entradas, misma salida, sin efectos secundarios. Son fáciles de testear.
- const por defecto,
letsolo si el valor cambia, y nuncavar. - Sal pronto: comprueba los casos raros al principio y termina con
return, en lugar de anidarifdentro deif.
La última regla es la que más cambia la lectura de una función. Compara:
// Antes: tres niveles de if
function precioFinal(usuario, carrito) {
if (usuario) {
if (carrito.length > 0) {
if (usuario.esPremium) {
return total(carrito) * 0.9;
}
return total(carrito);
}
}
return 0;
}
// Después: los casos raros salen primero
function precioFinal(usuario, carrito) {
if (!usuario || carrito.length === 0) return 0;
if (usuario.esPremium) return total(carrito) * 0.9;
return total(carrito);
}
Patrón Singleton
Garantiza que una clase tenga una sola instancia en toda la app. Útil para configuración global o una conexión compartida.
class Config {
constructor() {
if (Config.instancia) return Config.instancia;
this.datos = {};
Config.instancia = this;
}
}
const a = new Config();
const b = new Config();
console.log(a === b); // true: es la misma instancia
La versión moderna: el propio módulo
En JavaScript moderno casi nunca necesitas esa clase. Un módulo ES se evalúa una sola vez, y todos los import reciben el mismo objeto:
// config.js
const config = Object.freeze({ tema: 'oscuro', idioma: 'es' });
export default config;
// app.js
import config from './config.js';
import mismaConfig from './config.js';
console.log(config === mismaConfig); // true: el módulo se evalúa una sola vez
Object.freeze evita que alguien la modifique por accidente. Úsalo así, para datos de solo lectura. Un singleton con estado que cambia es estado global con otro nombre: complica depurar y los tests, porque herramientas como Jest o Vitest pueden cargar una instancia distinta en cada archivo de prueba.
Patrón Observer
Un objeto (el sujeto) mantiene una lista de observadores y les avisa cuando algo cambia. Es la base de la reactividad de muchos frameworks.
class Sujeto {
constructor() { this.observadores = []; }
suscribir(fn) { this.observadores.push(fn); }
notificar(dato) { this.observadores.forEach((fn) => fn(dato)); }
}
const noticias = new Sujeto();
noticias.suscribir((n) => console.log('Usuario 1:', n));
noticias.suscribir((n) => console.log('Usuario 2:', n));
noticias.notificar('¡Nueva publicación!');
A este ejemplo le falta una pieza: darse de baja. Si un componente se suscribe y luego desaparece, su función sigue en la lista. Se ejecuta para nada y ocupa memoria que nunca se libera. La solución habitual es que suscribir devuelva la forma de cancelar:
class Sujeto {
constructor() { this.observadores = []; }
suscribir(fn) {
this.observadores.push(fn);
return () => this.desuscribir(fn); // devuelve la forma de darse de baja
}
desuscribir(fn) {
this.observadores = this.observadores.filter((f) => f !== fn);
}
notificar(dato) { this.observadores.forEach((fn) => fn(dato)); }
}
const noticias = new Sujeto();
const darseDeBaja = noticias.suscribir((n) => console.log('Usuario 1:', n));
noticias.notificar('Nueva publicación'); // Usuario 1: Nueva publicación
darseDeBaja();
noticias.notificar('Otra más'); // ya no imprime nada
La versión nativa: EventTarget
El navegador ya trae un Observer completo: es el mismo sistema de addEventListener que usas con los clics. Con EventTarget y CustomEvent lo aplicas a tus propios datos, y funciona igual en Node:
const carrito = new EventTarget();
carrito.addEventListener('cambio', (e) => {
console.log('Productos en el carrito:', e.detail.total);
});
carrito.dispatchEvent(new CustomEvent('cambio', { detail: { total: 3 } }));
// Productos en el carrito: 3
Para darte de baja, removeEventListener con la misma función.
Patrón Factory
Una función (la fábrica) que crea objetos según lo que le pidas. Quien la usa no necesita saber cómo se construyen.
function crearUsuario(tipo) {
const roles = {
admin: { permisos: ['leer', 'escribir', 'borrar'] },
editor: { permisos: ['leer', 'escribir'] },
lector: { permisos: ['leer'] }
};
return { tipo, ...roles[tipo] };
}
const admin = crearUsuario('admin');
console.log(admin.permisos); // ['leer', 'escribir', 'borrar']
Prueba a pedirle un tipo que no existe: crearUsuario('invitado') devuelve { tipo: 'invitado' }, sin permisos y sin avisar. El fallo aparecerá mucho después, lejos de su origen. Una fábrica debe quejarse en el momento:
// usuarios.js
const roles = {
admin: { permisos: ['leer', 'escribir', 'borrar'] },
editor: { permisos: ['leer', 'escribir'] },
lector: { permisos: ['leer'] }
};
export function crearUsuario(tipo) {
if (!roles[tipo]) {
throw new Error(`Tipo de usuario desconocido: ${tipo}`);
}
return { tipo, ...roles[tipo] };
}
Además, el objeto roles sale de la función: ya no se crea de nuevo en cada llamada.
Patrón Strategy
Sirve para elegir entre varios comportamientos sin una cadena de if o un switch que crece cada vez que añades un caso. Cada comportamiento (la estrategia) es una función, y las guardas en un objeto:
// Antes: cada método de envío nuevo es otro if
function costeEnvio(metodo, peso) {
if (metodo === 'estandar') return 4.95;
if (metodo === 'urgente') return 9.95 + peso * 0.5;
if (metodo === 'recogida') return 0;
}
// Con Strategy: un objeto de funciones
const estrategiasEnvio = {
estandar: () => 4.95,
urgente: (peso) => 9.95 + peso * 0.5,
recogida: () => 0
};
function costeEnvio(metodo, peso) {
const estrategia = estrategiasEnvio[metodo];
if (!estrategia) {
throw new Error(`Método de envío no válido: ${metodo}`);
}
return estrategia(peso);
}
console.log(costeEnvio('urgente', 2)); // 10.95
Añadir un método nuevo es añadir una línea al objeto, sin tocar la función. Y cada estrategia se testea por separado.
Introducción al testing con Jest
Un test comprueba automáticamente que tu código hace lo que esperas. Jest es la herramienta más usada. Se instala con npm y escribes pruebas que verifican una función.
npm install --save-dev jest
// suma.js
export function suma(a, b) { return a + b; }
// suma.test.js
import { suma } from './suma.js';
test('suma 2 + 3 = 5', () => {
expect(suma(2, 3)).toBe(5);
});
Ojo: si lanzas este ejemplo tal cual con npx jest, falla. Lo he probado con Jest 30.5 en Node 22 y en Node 24. El error es «Must use import to load ES Module». Jest no ejecuta import y export sin configuración. La solución más corta tiene dos pasos:
- En tu
package.json, añade"type": "module". - Lanza Jest con la opción de módulos de Node, mejor como script:
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js". Luego,npm test.
Así pasa el test, aunque con un aviso de que esa opción de Node es experimental.
Sin instalar nada: el test runner de Node
Desde Node 20, el lenguaje trae un sistema de tests estable: node:test. Entiende import sin configurar nada. Así se testea la fábrica de antes:
// usuarios.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { crearUsuario } from './usuarios.js';
test('un editor puede escribir pero no borrar', () => {
const editor = crearUsuario('editor');
assert.ok(editor.permisos.includes('escribir'));
assert.ok(!editor.permisos.includes('borrar'));
});
test('un tipo desconocido lanza un error', () => {
assert.throws(() => crearUsuario('invitado'), /desconocido/);
});
Lo lanzas con node --test, que busca los archivos .test.js, y te dice cuántos pasan. En mi prueba: 2 tests, 2 pasan, 0 fallan. Si después cambias la fábrica y rompes los permisos del editor, el test te avisa al instante.
| Jest | Vitest | node:test | |
|---|---|---|---|
| Instalación | npm i -D jest | npm i -D vitest | Nada, viene con Node |
| import/export | Necesita configuración | Funciona tal cual | Funciona tal cual |
| Sintaxis | expect(x).toBe(y) | La misma que Jest | assert.equal(x, y) |
| Para qué | Proyectos que ya lo usan | Proyectos nuevos, sobre todo con Vite | Funciones sueltas, sin dependencias |
Empieza testeando tus funciones puras: son las más fáciles y las que más te protegen. Para montar el entorno de Node necesario, mira la guía de Node.js y npm.
Si quieres un libro de cabecera: «Código limpio» de Robert C. Martin es la referencia de buenas prácticas, y su segunda edición en español salió en junio de 2026 (Anaya). No es un libro de JavaScript, pero sus reglas sobre nombres, funciones y tests se aplican igual.
Preguntas frecuentes
¿Necesito patrones de diseño en proyectos pequeños?
No fuerces patrones donde no hacen falta: en un script pequeño, sobran. Su valor aparece cuando el código crece y necesitas orden. Conocerlos te ayuda a reconocer cuándo uno encaja de verdad.
¿Cuáles son los patrones de diseño más usados en JavaScript?
En el día a día, los cuatro de esta guía: el módulo (que hace de Singleton), Observer en forma de eventos, Factory y Strategy. Hay muchos más, como Decorator, Proxy o Adapter, pero conviene dominar estos antes de ampliar.
¿El Singleton es un antipatrón?
No por sí mismo, pero se abusa de él. Para configuración de solo lectura o una utilidad sin estado, va bien. Si guarda estado que cambia, se convierte en una variable global y complica los tests.
¿Jest, Vitest o Mocha?
Jest viene con todo integrado (aserciones, mocks, cobertura) y es el estándar en muchos proyectos, sobre todo con React. Vitest usa la misma sintaxis y funciona con import/export sin configurar nada. Mocha es más modular, pero requiere juntar más piezas. Para empezar hoy, Vitest o el node:test de Node.
¿Qué es una función pura?
Una que, con las mismas entradas, siempre devuelve la misma salida y no modifica nada de fuera (no toca variables globales ni el DOM). Son predecibles y facilísimas de testear.
Con estos patrones de diseño en JavaScript y unos cuantos tests ya escribes código de nivel profesional. Ponlo en práctica con los proyectos de JavaScript para practicar: en los intermedios, añade un par de tests. Tienes más atajos de sintaxis moderna en mis trucos de JavaScript, y el temario completo en la hoja de ruta de JavaScript.
¿Vas a montar esos proyectos?
La lógica la pones con JavaScript, pero la interfaz hay que maquetarla. Mi pack CSS gratis trae más de 40 estilos listos para copiar, botones incluidos. Apúntate y te lo envío al correo.
Como Afiliada de Amazon, obtengo ingresos por las compras adscritas que cumplen los requisitos. Esto no supone ningún coste adicional para ti.


