Tag: JavaScript

  • Optimización extrema de rendimiento y consumo de memoria en Next.js 16

    Optimización extrema de rendimiento y consumo de memoria en Next.js 16

    Hace unos meses recibí una llamada de emergencia de un equipo que acababa de desplegar su aplicación de comercio electrónico construida sobre Next.js. El servidor Node.js en producción colapsaba cada 4 horas con el temible mensaje FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory.

    Su solución temporal era programar un reinicio automático del contenedor Docker cada 3 horas. Un parche espantoso para disimular un problema de arquitectura grave.

    El equipo culpaba a Node.js y a los servidores de Vercel. Pero al auditar el perfil de memoria, descubrimos que los desarrolladores estaban reteniendo objetos gigantescos en la caché de Server Components y desbordando la memoria durante la hidratación de datos.

    Next.js 16 introduce avances masivos en la gestión de memoria y compilación, pero si no entiendes cómo funciona su motor bajo el capó, es ridículamente fácil introducir memory leaks en producción.

    El espejismo de los Server Components sin estado

    Existe el mito de que los React Server Components (RSC) son inmunes a las fugas de memoria porque se ejecutan en el servidor y solo envían HTML/JSON al cliente.

    La realidad es que en el servidor, cada petición HTTP mantiene en memoria el árbol de renderizado del componente hasta que se completa la respuesta. Si dentro de un Server Component:

    • Suscribes escuchadores de eventos globales que no se destruyen.
    • Almacenas buffers de imágenes o respuestas API masivas en variables fuera de la función del componente.
    • Abres conexiones de base de datos dentro del render sin un pool reutilizable.

    Estás acumulando megabytes de basura retenida en la memoria Heap de Node.js en cada petición de usuario.

    Como ya explicamos en nuestro análisis detallado sobre la reducción de memoria en builds de Next.js, separar la memoria del compilador de la memoria en tiempo de ejecución es el primer paso para diagnosticar estos fallos.

    3 Estrategias para Optimizar Next.js 16 en Producción

    1. Gestión Inteligente de Caché de Datos (unstable_cache & PPR)

    En Next.js 16, la caché de peticiones debe configurarse explícitamente utilizando etiquetas de revalidación (revalidateTag) en lugar de almacenar respuestas masivas en memoria global:

    import { unstable_cache } from 'next/cache';
    
    export const getProductoDestacado = unstable_cache(
      async (id: string) => {
        // Consulta limpia a la base de datos
        return await db.producto.findUnique({ where: { id } });
      },
      ['producto-destacado-key'],
      {
        revalidate: 3600, // Revalida cada hora en segundo plano
        tags: ['productos']
      }
    );
    

    2. Configurar Límites de Memoria en Turbopack y Node.js

    Para evitar que el proceso de build agote la RAM de tu servidor de integración continua (CI/CD) o contenedor de producción, configura los flags de memoria de forma estricta en tu package.json:

    {
      "scripts": {
        "dev": "next dev --turbo",
        "build": "NODE_OPTIONS='--max-old-space-size=4096' next build"
      }
    }
    

    3. Evitar el "Waterfall" en Renderizado Asíncrono

    Uno de los fallos de rendimiento más comunes en Server Components es ejecutar peticiones await secuenciales cuando podrían resolverse en paralelo:

    // ❌ MAL: Peticiones en cascada (waterfall), triplica el tiempo de respuesta y retención en memoria
    const usuario = await getUsuario(id);
    const pedidos = await getPedidos(id);
    const metricas = await getMetricas(id);
    
    // ✅ BIEN: Ejecución en paralelo con Promise.all
    const [usuario, pedidos, metricas] = await Promise.all([
      getUsuario(id),
      getPedidos(id),
      getMetricas(id)
    ]);
    

    Al aplicar programación defensiva en TypeScript, garantizas que cualquier fallo dentro de Promise.all sea capturado sin dejar promesas colgadas en el event loop.

    Monitoreo y Diagnóstico de Memoria

    Para auditar el consumo real de tu aplicación en desarrollo o staging:

    1. Ejecuta el servidor con el inspector habilitado: node --inspect node_modules/.bin/next start.
    2. Abre Chrome DevTools (chrome://inspect) y toma una instantánea del Heap (Heap Snapshot).
    3. Filtra por clases retenidas (Closure, System / Context) para identificar qué Server Components no están siendo liberados por el recolector de basura (Garbage Collector).

    Como destacamos en nuestras guías de graph engineering, mapear las dependencias entre módulos es la forma más limpia de aislar fugas de memoria.


    Optimizar el rendimiento en Next.js 16 no requiere magia; requiere disciplina en la gestión de datos asíncronos y una configuración adecuada de los límites de memoria.

    Si quieres dominar el desarrollo fullstack moderno con Next.js y arquitecturas de alto rendimiento, descubre los Cursos de Dominicode. Y si buscas resolver desafíos complejos de producción en comunidad con otros desarrolladores senior, te esperamos en Dominicode Labs.

    Preguntas frecuentes

    ¿Por qué mi build de Next.js se queda congelado consumiendo 100% de CPU?

    Suele deberse a la importación masiva de módulos con dependencias circulares o al procesamiento de imágenes gigantescas durante la generación estática (SSG). Limitar el número de páginas pre-renderizadas en build mediante generateStaticParams dinámico soluciona el problema.

    ¿Qué diferencia hay entre revalidatePath y revalidateTag?

    revalidatePath purga toda la caché asociada a una URL específica. revalidateTag es mucho más eficiente porque purga de forma quirúrgica solo los datos que comparten una etiqueta concreta en todo el proyecto, sin invalidar otras secciones de la página.

    ¿Cómo afecta el uso de middleware al rendimiento en Next.js?

    El Middleware se ejecuta en el Edge Runtime antes de cada petición. Si realizas llamadas pesadas a APIs o consultas directas a bases de datos dentro del middleware, añadirás latencia a todas las rutas de tu aplicación. Mantén el middleware ultraligero (solo para redirecciones y lectura de headers/cookies).

    ¿Es recomendable usar next/image para todas las imágenes?

    Sí. El componente next/image optimiza automáticamente el formato (WebP/AVIF), ajusta las dimensiones según la pantalla del cliente y evita desplazamientos de diseño (Cumulative Layout Shift – CLS), reduciendo drásticamente la carga de memoria en el navegador.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Construyendo sitios web ultrarrápidos con Astro y Server Islands: Cero JS por defecto

    Construyendo sitios web ultrarrápidos con Astro y Server Islands: Cero JS por defecto

    Hace unos meses analicé la landing page de un cliente que ofrecía un producto SaaS. Habían construido la web utilizando un marco de trabajo de aplicación de página única (SPA) completo.

    Para renderizar un titular estático, una lista de precios y tres testimonios de clientes, el navegador del usuario tenía que descargar, descompilar y ejecutar 480 KB de JavaScript. En conexiones móviles 4G, el tiempo hasta que la página se volvía interactiva (Time to Interactive) superaba los 5.5 segundos. El resultado en Google Lighthouse era un doloroso 44/100.

    Perdían el 30% de los visitantes antes de que la página terminara de cargar.

    Al refactorizar el sitio hacia Astro y aprovechar la nueva funcionalidad de Server Islands, redujimos el bundle de JavaScript cliente para la estructura estática a 0 KB, logrando una puntuación de 100/100 en Core Web Vitals en el primer intento.

    La paradoja de enviar JavaScript para renderizar HTML

    Durante la última década, la industria del desarrollo web cometió un error colectivo: asumir que cualquier sitio web moderno debía empaquetarse dentro de una aplicación de React o Angular que se ejecuta íntegramente en el navegador del usuario.

    El resultado ha sido la degradación del rendimiento web:

    • El navegador descarga megabytes de JavaScript para crear nodos de DOM que podrían haber sido enviados directamente como HTML estático.
    • La CPU del dispositivo móvil se satura ejecutando hidratación de estado.
    • Los motores de búsqueda e intenciones de búsqueda sufren retardos de indexación.

    Astro invirtió este modelo con su filosofía "Zero JavaScript by default" (Cero JavaScript por defecto). Astro renderiza todo el componente a HTML estático en el servidor y solo envía JavaScript al cliente si especificas explícitamente una isla interactiva (Islands Architecture).

    ¿Qué son las Server Islands en Astro?

    La arquitectura de islas tradicional permitía incrustar componentes interactivos cliente (React, Vue, Svelte) dentro de una página estática usando directivas como client:load o client:visible.

    Sin embargo, las Server Islands introducen un avance superior: permiten posponer la renderización de un componente dinámico de servidor sin bloquear la carga estática inicial de la página.

    ┌─────────────────────────────────────────────────────────┐
    │ HTML Estático enviado de inmediato (TTFB ultra bajo)     │
    │ ┌─────────────────────────────────────────────────────┐ │
    │ │ Hero Section + Menú + Testimonios (HTML Puro)       │ │
    │ └─────────────────────────────────────────────────────┘ │
    │ ┌─────────────────────────────────────────────────────┐ │
    │ │ <AvatarUsuario server:defer /> (Server Island)      │ │
    │ │  └─► Renderiza fallback estático instantáneo        │ │
    │ │  └─► Se sustituye en segundo plano por HTML del srv  │ │
    │ └─────────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────┘
    

    Ejemplo de uso de Server Island en Astro

    Imagina un blog de alta velocidad donde la mayor parte del contenido es estático, pero deseas mostrar el avatar personalizado del usuario autenticado en la barra superior.

    ---
    // src/pages/posts/[slug].astro
    import Layout from '../layouts/Layout.astro';
    import AvatarUsuario from '../components/AvatarUsuario.astro';
    import ContenidoPost from '../components/ContenidoPost.astro';
    
    const { slug } = Astro.params;
    ---
    
    <Layout title="Post de Blog Ultrarrápido">
      <header style="display: flex; justify-content: space-between;">
        <Logo />
        <!-- La Server Island no bloquea la carga de la página estática -->
        <AvatarUsuario server:defer>
          <!-- Fallback mientras el servidor procesa la sesión -->
          <div slot="fallback" class="avatar-skeleton"></div>
        </AvatarUsuario>
      </header>
    
      <main>
        <ContenidoPost slug={slug} />
      </main>
    </Layout>
    

    Al cargar la página:

    1. El servidor entrega HTML puro súper rápido (la estructura completa del artículo y la plantilla).
    2. El cliente ve la página cargada de forma instantánea con el skeleton del avatar.
    3. Astro ejecuta en segundo plano el componente <AvatarUsuario /> en el servidor y reemplaza el fallback con el HTML dinámico parseado sin necesidad de descargar una pesada librería cliente.

    Como vimos al comparar el consumo en tiempo de compilación con Next.js y Turbopack, utilizar la arquitectura correcta para cada tipo de proyecto es la decisión de rendimiento más rentable.

    Tipado Defensivo y Colecciones de Contenido

    Astro integra Content Collections, un sistema basado en Zod que valida en tiempo de compilación que todos tus archivos Markdown o MDX cumplan exactamente con la estructura de tipos definida.

    // src/content/config.ts
    import { defineCollection, z } from 'astro:content';
    
    const postsCollection = defineCollection({
      type: 'content',
      schema: z.object({
        title: z.string(),
        description: z.string().max(160),
        pubDate: z.date(),
        author: z.string().default('Bezael Pérez'),
        tags: z.array(z.string()),
      }),
    });
    
    export const collections = { posts: postsCollection };
    

    Al aplicar programación defensiva en TypeScript, garantizas que ningún artículo con metadatos defectuosos rompa la generación estática de tu sitio web.

    Además, mantener aisladas las dependencias de tus componentes siguiendo principios de graph engineering permite reutilizar componentes de React o Vue dentro de Astro de manera impecable.


    Astro y sus Server Islands representan la convergencia perfecta entre la velocidad extrema del HTML estático y la flexibilidad de la web dinámica moderna.

    Si quieres dominar el desarrollo web moderno, optimización de rendimiento y arquitectura frontend, explora los Cursos de Dominicode. Y si buscas construir sitios web y productos de alto impacto junto a desarrolladores senior, súmate a Dominicode Labs.

    Preguntas frecuentes

    ¿En qué se diferencia una Server Island de un Server Component de React?

    Los Server Components de React requieren que toda la aplicación comparta el modelo de hidratación y empaquetado de React. Las Server Islands de Astro son agnósticas al framework: puedes usar componentes en Astro puro, React, Vue, Svelte o Solid, y se reemplazan de forma asíncrona mediante un fragmento de HTML ligero sin cargar el runtime del framework si no es necesario.

    ¿Puedo seguir usando componentes interactivos de React en Astro?

    Sí. Puedes importar cualquier componente de React, Vue o Svelte en Astro. Para habilitar la interactividad cliente en un componente específico, solo añades la directiva de hidratación correspondiente, como client:visible (se hidrata solo cuando el usuario hace scroll hasta él) o client:idle (se hidrata cuando el navegador está inactivo).

    ¿Server Islands requiere una plataforma de despliegue en servidor (SSR)?

    Para que las Server Islands funcionen procesando peticiones dinámicas en segundo plano, tu proyecto Astro debe desplegarse con un adaptador SSR (Server-Side Rendering) en plataformas como Vercel, Netlify, Cloudflare Workers o un contenedor Docker con Node.js/Bun.

    ¿Astro es adecuado para aplicaciones web complejas con paneles de administración?

    Astro es imbatible para sitios web centrados en contenido, blogs, e-commerce, documentación y landing pages. Para paneles de administración interactivos con estado denso en cliente (dashboards complejos), combinar Astro para las páginas públicas con un framework como Next.js, Angular o React para el panel privado es una excelente estrategia de arquitectura.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Gestión de estado global sin dolor combinando Zod y Signals en aplicaciones modernas

    Gestión de estado global sin dolor combinando Zod y Signals en aplicaciones modernas

    Hace un par de años audité una aplicación enterprise en React y TypeScript que utilizaba Redux Toolkit. Para gestionar el estado de 6 pantallas principales, el equipo había tenido que escribir más de 3.500 líneas de código entre actions, reducers, selectors y Middlewares de Thunk.

    Lo grave no era la cantidad de archivos. Lo grave era que cuando el backend cambiaba un campo opcional de la API sin avisar, el store de Redux aceptaba el objeto corrupto y la aplicación explotaba páginas más tarde con el temible Cannot read properties of undefined.

    Habían creado un sistema complejo que no ofrecía ninguna protección real en tiempo de ejecución.

    La combinación de Zod (validación de esquemas) y Signals (reactividad de grano fino) se ha convertido en el estándar moderno para eliminar el dolor de la gestión de estado global en aplicaciones frontend.

    El problema de las librerías de estado tradicionales

    Durante años creímos que para gestionar el estado de una aplicación web necesitábamos un contenedor monolítico global con patrones de inmutabilidad estrictos.

    Ese enfoque sufría tres defectos estructurales:

    1. Verbosidad extrema: Escribir decenas de funciones de selección y mutación para actualizar una simple propiedad de usuario.
    2. Re-renderizados innecesarios: Si un componente escuchaba un objeto de estado global grande, cualquier cambio menor provocaba el re-renderizado del árbol de UI completo.
    3. Ceguera en la frontera API: Asumir que la respuesta del backend coincide al 100% con los tipos de TypeScript sin validar los datos entrantes.

    Como destacamos en nuestro artículo sobre programación defensiva en TypeScript, las interfaces de TypeScript desaparecen al transpilar, por lo que confiar solo en tipos en tiempo de compilación es una trampa.

    La Arquitectura Zod + Signals

    La solución moderna consiste en aplicar la validación de esquemas en la frontera de entrada (HTTP) y gestionar la reactividad atómica mediante Signals (disponibles de forma nativa en Angular, Preact, SolidJS o mediante librerías ultraligeras como @preact/signals en React).

    ┌─────────────────────────────────────────────────────────┐
    │ Respuesta API HTTP (JSON sin confiar)                   │
    │  └─► Validacion en tiempo de ejecucion con Zod Schema   │
    │     ┌───────────────────────────────────────────────────┐
    │     │ Estado Reactivo Atómico (Signals)                │
    │     │  └─► signal(), computed()                         │
    │     └───────────────────────────────────────────────────┘
    │  ┌──────────────────────────────────────────────────────┐
    │  │ Componentes de UI (Actualización Quirúrgica)         │
    │  └──────────────────────────────────────────────────────┘
    └─────────────────────────────────────────────────────────┘
    

    1. Definición del Esquema Zod y Tipado Automático

    import { z } from 'zod';
    
    // 1. Esquema con validación estricta en tiempo de ejecución
    export const UserStateSchema = z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      nombre: z.string().min(2),
      rol: z.enum(['ADMIN', 'USER', 'GUEST']),
      preferencias: z.object({
        tema: z.enum(['light', 'dark']).default('dark'),
      }),
    });
    
    // Inferir el tipo de TypeScript automáticamente
    export type UserState = z.infer<typeof UserStateSchema>;
    

    2. Store Reactivo basado en Signals

    import { signal, computed } from '@preact/signals-react';
    import { UserStateSchema, UserState } from './user.schema';
    
    // State atómico inicial
    export const usuarioSignal = signal<UserState | null>(null);
    export const estaAutenticadoSignal = computed(() => usuarioSignal.value !== null);
    export const esAdminSignal = computed(() => usuarioSignal.value?.rol === 'ADMIN');
    
    // Acción de actualización con validación Zod defensiva
    export function setUsuarioConValidacion(rawData: unknown) {
      const parseResult = UserStateSchema.safeParse(rawData);
    
      if (!parseResult.success) {
        console.error('Payload de API inválido:', parseResult.error.format());
        // Se evita corromper el estado global con datos inválidos
        return false;
      }
    
      // Se asigna únicamente si la validación es 100% exitosa
      usuarioSignal.value = parseResult.data;
      return true;
    }
    

    Beneficios en Aplicaciones de Producción

    1. Re-renderizados quirúrgicos: Al consumir esAdminSignal en un botón de administración, solo ese botón se re-evalúa cuando el rol cambia. El resto de la UI permanece intacta sin necesidad de memoizaciones manuales (useMemo, React.memo).
    2. Cero corrupción de estado: Si la API devuelve un campo mal formateado, Zod detiene la propagación en la frontera HTTP antes de que afecte a la reactividad de la aplicación.
    3. Escalabilidad de código: Eliminas más del 70% del boilerplate de Redux/MobX, creando un código limpio que tanto los desarrolladores como los asistentes de IA pueden refactorizar sin riesgo.

    Al estructurar los módulos de estado siguiendo los principios de graph engineering, consigues una separación clara entre la lógica de datos y los componentes de presentación.

    Y si estás desarrollando en Angular, ten en cuenta el constante ciclo de releases de Angular donde los Signals y los Signal Forms se han integrado como el estándar nativo del framework.


    Simplificar la gestión de estado combinando la solidez de Zod con la velocidad de los Signals permite construir interfaces mantenibles, reactivas y blindadas ante fallos de producción.

    Si quieres dominar el desarrollo frontend moderno y las mejores prácticas de arquitectura con TypeScript, explora los Cursos de Dominicode. Y si quieres construir aplicaciones reales junto a otros desarrolladores senior, te esperamos en Dominicode Labs.

    Preguntas frecuentes

    ¿Puedo usar Zod con otras librerías de estado como Zustand o Pinia?

    Sí. Zod es una librería de validación agnóstica al framework. Puedes usar ZodSchema.parse() dentro de las acciones de Zustand, Pinia, Redux o cualquier otra librería para validar los datos antes de guardarlos en el store.

    ¿Qué diferencia hay entre la reactividad de Signals y los Observables de RxJS?

    Los Signals están optimizados para la reactividad síncrona de UI con evaluación perezosa y seguimiento automático de dependencias. RxJS está diseñado para la coordinación de eventos asíncronos en el tiempo (peticiones HTTP, WebSockets, timers). En aplicaciones modernas, se usan Signals para el estado del componente y RxJS para streams asíncronos.

    ¿Zod añade demasiado peso al bundle del cliente?

    No. Zod es una librería ultraligera (menos de 12 KB gzippeado) y soporta tree-shaking, por lo que solo se empaquetan en el cliente los métodos y validadores que utilices explícitamente en tu código.

    ¿Cómo persiste el estado basado en Signals entre recargas de página?

    Puedes crear un efecto reactivo que sincronice automáticamente el valor del Signal con localStorage o sessionStorage cada vez que el Signal cambia, parseando los datos con Zod al restaurar la sesión.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Entornos de desarrollo reproducibles con Docker y Dev Containers: Adiós al ‘en mi máquina funciona’

    Entornos de desarrollo reproducibles con Docker y Dev Containers: Adiós al ‘en mi máquina funciona’

    Hace un año incorporamos a un desarrollador senior a un proyecto que integraba microservicios en Node.js 22, utilidades en Python 3.12, PostgreSQL con la extensión pgvector y colas de tareas en Redis.

    El proceso de Onboarding para que el nuevo desarrollador pudiera ejecutar la aplicación en su portátil local duró dos días completos.

    Hubo conflictos de versiones entre NVM, diferencias en el compilador de C++ para binarios nativos en macOS M3 frente a Windows, e incompatibilidades de variables de entorno. Durante esos dos días, dos ingenieros senior tuvieron que detener su trabajo para hacer pair programming y desatascar la instalación.

    Dos semanas después configuramos Dev Containers (.devcontainer/devcontainer.json).

    Cuando se sumó el siguiente desarrollador al equipo, clonó el repositorio, abrió VS Code / Cursor, hizo clic en "Reopen in Container", y en 3 minutos y 40 segundos tenía la aplicación corriendo con la base de datos poblada y todos los linters configurados.

    Tener entornos de desarrollo reproducibles con Docker no es una preferencia cosmética; es la diferencia entre un equipo acelerado y una pesadilla de soporte técnico interno.

    ¿Qué son los Dev Containers y por qué superan a Docker Compose?

    Durante años intentamos solucionar el problema de las versiones en local usando docker-compose.yml.

    Aunque Docker Compose resolvía la ejecución de servicios de fondo (bases de datos o cachés), el código fuente principal seguía ejecutándose en el sistema operativo del anfitrión (Host OS). Tu editor de código utilizaba los binarios de Node.js o Python instalados en tu máquina local, lo que provocaba discrepancias en el intellisense, formatos de línea y extensiones del IDE.

    La especificación Dev Containers (estandarizada por la Development Containers Specification de la Linux Foundation) va un paso más allá: mueve todo el entorno de desarrollo (incluyendo el servidor del editor, terminal, extensiones y compiladores) dentro de un contenedor de Docker aislado.

    ┌─────────────────────────────────────────────────────────┐
    │ Máquina Anfitriona (macOS / Windows / Linux)            │
    │ ┌─────────────────────────────────────────────────────┐ │
    │ │ Editor de Código (VS Code / Cursor Client UI)       │ │
    │ └──────────────────────────┬──────────────────────────┘ │
    │                            │ SSH / Socket RPC           │
    │ ┌──────────────────────────▼──────────────────────────┐ │
    │ │ Contenedor Docker (Linux Debian/Ubuntu)             │ │
    │ │ ┌─────────────────────────────────────────────────┐ │ │
    │ │ │ VS Code Server + Extensiones + Linters           │ │ │
    │ │ ├─────────────────────────────────────────────────┤ │ │
    │ │ │ Node.js 22 + Python 3.12 + TypeScript + Bun     │ │ │
    │ │ ├─────────────────────────────────────────────────┤ │ │
    │ │ │ Código Fuente Montado + PostgreSQL + Redis       │ │ │
    │ │ └─────────────────────────────────────────────────┘ │ │
    │ └─────────────────────────────────────────────────────┘ │
    └─────────────────────────────────────────────────────────┘
    

    Configuración Paso a Paso de un Dev Container Profesional

    Para convertir cualquier proyecto en un entorno reproducible, solo necesitas crear una carpeta .devcontainer en la raíz del repositorio con los siguientes dos archivos.

    1. .devcontainer/docker-compose.yml

    version: '3.8'
    
    services:
      app:
        build:
          context: .
          dockerfile: Dockerfile
        volumes:
          - ..:/workspace:cached
        command: sleep infinity
        network_mode: service:db
    
      db:
        image: pgvector/pgvector:pg16
        environment:
          POSTGRES_DB: dev_db
          POSTGRES_USER: dev_user
          POSTGRES_PASSWORD: dev_password
        ports:
          - "5432:5432"
    

    2. .devcontainer/devcontainer.json

    {
      "name": "Dominicode Fullstack Dev Environment",
      "dockerComposeFile": "docker-compose.yml",
      "service": "app",
      "workspaceFolder": "/workspace",
      "customizations": {
        "vscode": {
          "settings": {
            "editor.formatOnSave": true,
            "editor.defaultFormatter": "esbenp.prettier-vscode",
            "typescript.tsdk": "node_modules/typescript/lib"
          },
          "extensions": [
            "dbaeumer.vscode-eslint",
            "esbenp.prettier-vscode",
            "eamodio.gitlens",
            "biomejs.biome"
          ]
        }
      },
      "remoteUser": "node",
      "postCreateCommand": "bun install"
    }
    

    Ventajas Clave para Equipos de Desarrollo e IA

    1. Onboarding en 1 Clic: Cualquier nuevo desarrollador (o agente de IA que opere en entornos remotos) puede levantar un workspace 100% funcional sin instalar dependencias globales en su máquina.
    2. Homogeneización de herramientas: Todo el equipo comparte exactamente la misma versión de Node.js, TypeScript y linters. Nadie puede subir código formateado con reglas distintas.
    3. Aislamiento Total: Puedes trabajar simultáneamente en dos proyectos que utilicen versiones totalmente incompatibles de bases de datos o ejecutables de sistema sin que interfieran entre sí.

    Como analizamos en nuestra guía sobre cómo formar a tu equipo de desarrollo en IA, estandarizar los entornos de trabajo es el primer paso para acelerar la adopción de herramientas avanzadas.

    Al escribir código dentro del contenedor, sigues aplicando principios de programación defensiva en TypeScript, con la tranquilidad de que las validaciones de tipo en tiempo de ejecución se comportarán exactamente igual en local y en integración continua.

    Además, mantener aislados los límites de dependencias de tus servicios facilita aplicar estrategias de graph engineering para mapear tu arquitectura.


    Decir adiós al "en mi máquina funciona" es posible. Adoptar Dev Containers eleva la madurez de tu ingeniería de software y ahorra cientos de horas de soporte innecesario.

    Si quieres dominar las mejores prácticas de DevOps, arquitectura y desarrollo moderno con TypeScript, explora los Cursos de Dominicode. Y si quieres colaborar en proyectos reales junto a desarrolladores senior, súmate a Dominicode Labs.

    Preguntas frecuentes

    ¿Afecta el rendimiento de lectura de disco al usar Dev Containers en macOS o Windows?

    En macOS y Windows, Docker ejecuta una máquina virtual ligera. Para garantizar una velocidad de lectura de archivos óptima en los volúmenes de código fuente, la especificación usa montajes con flag :cached o tecnologías como VirtioFS en macOS, ofreciendo un rendimiento prácticamente idéntico al nativo.

    ¿Puedo usar Dev Containers si programo en Cursor o WebStorm?

    Sí. Cursor soporta la especificación Dev Containers de forma nativa al estar basado en VS Code. JetBrains (WebStorm, IntelliJ) también ofrece soporte completo para Dev Containers a través de su arquitectura de desarrollo remoto.

    ¿Qué ocurre con mis credenciales de Git y claves SSH?

    Dev Containers reenvía automáticamente el agente SSH (SSH Agent Forwarding) y las credenciales de Git de tu máquina anfitriona al interior del contenedor. Puedes hacer git commit y git push desde la terminal del contenedor usando tus claves privadas de forma segura sin copiarlas dentro de la imagen.

    ¿Dev Containers es útil para proyectos pequeños de un solo desarrollador?

    Absolutamente. Además de evitar contaminar tu sistema operativo con decenas de versiones globales de bases de datos o servicios, te permite formatear tu portátil o cambiar de equipo informático y retomar exactamente el mismo estado de desarrollo en minutos.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Cómo escribir y publicar tu propio libro técnico en Amazon KDP siendo programador

    Cómo escribir y publicar tu propio libro técnico en Amazon KDP siendo programador

    Durante años pensé que para publicar un libro de programación necesitabas ser contratado por una gran editorial técnica, enviar propuestas durante meses y aceptar que te pagaran un miserable 8% de regalías un año después de escribirlo.

    Cuando publiqué mis primeros libros técnicos de forma autodidacta usando Markdown y los subí directamente a Amazon KDP (Kindle Direct Publishing), me di cuenta de lo equivocado que estaba.

    Publicar un libro técnico no solo te reporta ingresos pasivos mes a mes. Es la mayor carta de presentación posible para tu carrera como desarrollador: te posiciona de inmediato como un referente en tu tecnología, abre puertas para consultorías de alto valor y multiplica tu autoridad profesional.

    Si sabes programar y has resuelto problemas reales en producción, ya tienes todo lo necesario para escribir y publicar tu propio libro técnico.

    Por qué escribir un libro en la era de la IA

    Con la proliferación de contenido generado automáticamente en internet, el valor de la voz de un desarrollador senior con experiencia real ha aumentado drásticamente.

    Como planteamos en nuestro análisis sobre si la IA va a sustituir a los programadores, el valor del mercado ya no está en escupir código sintácticamente correcto, sino en la capacidad de estructurar sistemas, explicar decisiones de arquitectura y enseñar soluciones probadas en batalla.

    Un libro técnico bien empaquetado transmite la experiencia práctica que un desarrollador busca cuando quiere aprender un framework sin perder semanas probando tutoriales desactualizados.

    El Stack del Desarrollador Escritor

    Olvídate de Microsoft Word o Indesign. Como programadores, nuestro entorno de trabajo habitual es ideal para redactar libros técnicos:

    ┌─────────────────────────────────────────────────────────┐
    │ Redacción en Markdown (VS Code / Claude Code)           │
    │  └─► Control de versiones con Git & GitHub             │
    │     ┌───────────────────────────────────────────────────┐
    │     │ Compilación de formatos (Pandoc / CSS / HTML)     │
    │     │  └─► Salida: PDF (Imprenta) + EPUB (Kindle)      │
    │     └───────────────────────────────────────────────────┘
    │  ┌──────────────────────────────────────────────────────┐
    │  │ Distribución Directa (Amazon KDP + Leanpub)          │
    │  └──────────────────────────────────────────────────────┘
    └─────────────────────────────────────────────────────────┘
    
    1. Redacción: Archivos .md organizados por capítulos en Git.
    2. Control de Código: Fragmentos de código reales probados con tests unitarios en el mismo repositorio.
    3. Formateo Automatizado: Scripts en CLI (usando Pandoc, PrinceXML o Puppeteer) para compilar el Markdown a PDF listo para impresión y EPUB para lectores digitales.

    El flujo de trabajo: De la idea a las regalías en Amazon

    1. Validación rápida (Validar antes de escribir 300 páginas)

    Antes de redactar el libro completo, escribe una tabla de contenidos detallada y publica un primer borrador o guía en plataformas como Leanpub o Gumroad. Si los primeros desarrolladores compran la versión preliminar, tienes luz verde.

    2. Estructuración pedagógica

    Un buen libro técnico no es una documentación de API traducida. Es una ruta estructurada de aprendizaje. Al igual que en nuestra metodología para formar a un equipo de desarrollo en IA en 6 semanas, debes ir de lo conceptual a lo práctico con proyectos reales paso a paso.

    3. Portada y formateo de Amazon KDP

    En Amazon KDP la portada es el 50% de la conversión. Diseña una portada limpia, con alto contraste y tipografía profesional. Asegúrate de ajustar las sangrías y márgenes de corte (bleed) según las especificaciones de Amazon si vas a ofrecer versión en papel (Paperback/Hardcover).

    4. Regalías y Estrategia de Precio

    Amazon KDP ofrece hasta un 70% de regalías en versión Kindle digital y un 60% en versión física de tapa blanda. Establecer tu precio entre $9.99 y $24.99 en digital suele ofrecer el mejor equilibrio entre volumen de ventas e ingresos netos.


    Publicar tu primer libro técnico es un proyecto de fin de semana acelerado que pagará dividendos durante años en tu carrera.

    Si te interesa profundizar en la creación de productos técnicos y modelos de ingresos para desarrolladores, explora los Cursos de Dominicode. Y si quieres rodearte de creadores y builders que están lanzando sus propias herramientas y publicaciones, únete a Dominicode Labs.

    Preguntas frecuentes

    ¿Necesito pedir ISBN antes de publicar en Amazon KDP?

    No. Amazon KDP te proporciona un código ASIN gratuito para libros digitales y un ISBN gratuito para las versiones impresas dentro de su plataforma. Si deseas vender el mismo formato físico en librerías externas, puedes comprar tu propio ISBN.

    ¿Cuánto tiempo lleva escribir un libro técnico de 150 páginas?

    Con una estructura clara y dedicando de 4 a 6 horas semanales, un desarrollador puede completar un libro técnico enfocado en 6 u 8 semanas.

    ¿Puedo usar asistentes de IA para ayudarme a redactar el libro?

    Sí, herramientas como Claude Code son excelentes para estructurar esquemas de capítulos, sugerir ejercicios prácticos y revisar la gramática de tus explicaciones. No obstante, el valor principal debe provenir de tus ejemplos de código y experiencia real.

    ¿Qué diferencia hay entre publicar en KDP e ir con una editorial como O'Reilly o Packt?

    Ir con una editorial tradicional te ofrece prestigio de marca, pero el proceso tarda entre 9 y 18 meses y solo recibes entre el 8% y el 15% de regalías. En KDP publicas de forma instantánea, mantienes el 100% de los derechos y conservas hasta el 70% de los ingresos.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Cómo construir micro-SaaS rentables operando como solo developer asistido por IA

    Cómo construir micro-SaaS rentables operando como solo developer asistido por IA

    Hace tres años, lanzar un producto de software como servicio (SaaS) requería un equipo entero. Necesitabas un desarrollador frontend, un ingeniero backend, un diseñador UI/UX, un especialista en bases de datos, un responsable de QA y un copywriter para las landing pages. Si intentabas hacerlo todo solo, tardabas 9 meses en sacar una versión alfa.

    Hoy, la combinación de stack moderno (Next.js, Supabase, Stripe) y agentes de desarrollo acelerados con IA me permite operar múltiples líneas de negocio en solitario.

    Construir micro-SaaS rentables siendo solo developer asistido por IA ya no es una fantasía de hackers de fin de semana. Es el modelo de negocio más eficiente para ingenieros senior que quieren crear independencia financiera acumulando ingresos recurrentes (MRR) sin la sobrecarga de gestionar personal.

    El cambio de paradigma: Del equipo tradicional al "Solo Builder" con IA

    Un micro-SaaS es una herramienta de software hiperenfocada que resuelve un problema específico para un nicho bien definido. No buscas levantar rondas de capital riesgo ni contratar a 50 empleados. Buscas un producto que facture entre 2,000 $ y 15,000 $ al mes con costes operativos mínimos.

    Antes de la IA, el cuello de botella del solo builder era la falta de horas en el día. Tenías que pasar de escribir código backend a configurar pipelines de CI/CD, diseñar thumbnails u redactar emails de venta.

    Con agentes de IA especializados (como Claude Code, AGY o subagentes configurados en tu entorno):

    • Delegas el formateo de componentes, la generación de tests y el código boilerplate.
    • Redactas copys persuasionales para tus páginas de captación en minutos.
    • Automatizas las tareas repetitivas de mantenimiento y atención al cliente.

    Como planteamos en nuestro análisis sobre si la IA va a sustituir a los programadores, la ventaja competitiva ha dejado de estar en picar código a mano y ha pasado a la capacidad de orquestar soluciones de producto completas.

    El Stack Tecnológico del Solo Developer en 2026

    Para operar un micro-SaaS sin morir en el intento, tu arquitectura debe ser ultraligera y requerir cero mantenimiento de servidores:

    ┌─────────────────────────────────────────────────────────┐
    │ Frontend & Rendering: Next.js / Astro / TailwindCSS     │
    │  └─► Desplegado en Vercel o Netlify (Cero DevOps)       │
    │     ┌───────────────────────────────────────────────────┐
    │     │ Backend & Data: Supabase / PostgreSQL / Hono      │
    │     │  └─► Auth, DB, Edge Functions y Vector Search    │
    │     └───────────────────────────────────────────────────┘
    │  ┌──────────────────────────────────────────────────────┐
    │  │ Pagos & Facturación: Stripe / Lemon Squeezy         │
    │  │  └─► Subscripciones recurrentes y webhooks           │
    │  └──────────────────────────────────────────────────────┘
    └─────────────────────────────────────────────────────────┘
    
    1. Frontend: Next.js o Astro para maquetación e hidratación ultrarrápida.
    2. Base de Datos & Auth: Supabase o Neon sobre PostgreSQL. Te proporciona autenticación, base de datos relacional y storage sin gestionar servidores.
    3. Monetización: Stripe para gestionar suscripciones recurrentes, pagos en un clic y portales de clientes automáticos.
    4. Asistente de Desarrollo: Claude Code y subagentes integrados para generar tareas, refactorizar módulos y escribir especificaciones antes de codificar.

    Las 3 Reglas de Oro para Construir Micro-SaaS Rentables

    1. Valida el problema antes de escribir código

    El error número uno de los desarrolladores es encerrarse a programar durante 3 meses para descubrir que nadie quiere pagar por el producto. Crea una landing page simple, comparte la propuesta en redes o comunidades técnicas y verifica si hay intención de pago real.

    Como explicamos en nuestra guía sobre cuándo NO usar Spec-Driven Development, en fases muy tempranas de exploración debes priorizar la velocidad de aprendizaje sobre arquitecturas sobre-diseñadas.

    2. Controla los costes de tokens e infraestructura de IA

    Si tu micro-SaaS incluye funcionalidades basadas en LLMs (ej. resúmenes automáticos, asistentes RAG o generación de imágenes), diseña la arquitectura pensando en los márgenes de beneficio.

    Como analizamos al calcular el coste de subagentes al cambiar de modelo, elegir el modelo adecuado para cada tarea (usando modelos ligeros como Haiku o Flash para tareas simples y modelos avanzados para razonamiento complejo) es la diferencia entre tener un margen del 80% o perder dinero en cada registro.

    3. Automatiza la retención y el soporte desde el día 1

    Como desarrollador en solitario, tu activo más valioso es tu tiempo. Configura secuencias de onboarding por email automáticas y asistentes de soporte basados en documentación para resolver las dudas más frecuentes sin intervención manual.


    Construir micro-SaaS rentables te da la libertad de trabajar en tus propios términos, aplicando tu experiencia técnica en productos que aportan valor real a tus clientes.

    Si quieres aprender las mejores técnicas de desarrollo frontend, backend y arquitectura con IA, explora los Cursos de Dominicode. Y si quieres unirte a una comunidad privada de creadores que están construyendo y facturando con sus propios proyectos de software, te esperamos en Dominicode Labs.

    Preguntas frecuentes

    ¿Cuánto dinero se necesita para lanzar un micro-SaaS?

    Gracias al tier gratuito de plataformas como Vercel, Supabase, GitHub y Stripe, el coste inicial de infraestructura para lanzar un micro-SaaS es prácticamente cero dólares al mes. Tus únicos gastos iniciales son el nombre de dominio (unos 10 $/año) y tu suscripción a herramientas de IA.

    ¿Cuánto tiempo lleva desarrollar un MVP (Producto Mínimo Viable)?

    Usando el stack recomendado y apoyándote en asistentes de IA para el código repetitivo, un desarrollador senior puede construir y lanzar un MVP funcional en 2 a 4 semanas trabajando a tiempo parcial.

    ¿Cómo competir contra grandes empresas siendo un solo developer?

    Tu ventaja es la agilidad y el enfoque de nicho. Una gran empresa no puede dedicar recursos a resolver un problema específico de 5,000 $ de MRR para una industria concreta. Tú puedes construir una solución a medida, ofrecer una atención cercana y adaptar el producto en horas sin pasar por comités de aprobación.

    ¿Se pueden vender estos micro-SaaS en el futuro?

    Sí. Existe un mercado secundario enorme en plataformas como Acquire.com o Flippa donde inversores compran micro-SaaS validados y rentables por múltiplos de 3x a 5x de sus ingresos anuales (ARR).


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Pruebas unitarias ultrarrápidas con Vitest en proyectos de frontend e IA

    Pruebas unitarias ultrarrápidas con Vitest en proyectos de frontend e IA

    Hace un tiempo audité el repositorio de un proyecto en React y TypeScript que contaba con una suite de 400 pruebas unitarias en Jest. Cada vez que lanzabas npm test, tardaba 4 minutos y 15 segundos en completar la ejecución.

    Los desarrolladores habían dejado de ejecutar los tests en local. Su flujo de trabajo era hacer git push y esperar a que el servidor de integración continua (CI) les dijera 10 minutos después si habían roto algo.

    La fricción era enorme.

    Cuando reemplazamos Jest por Vitest y optimizamos el entorno de DOM con happy-dom, la misma suite de 400 tests pasó de tardar 4 minutos a completarse en 7.8 segundos.

    En la era del desarrollo acelerado con IA, tener un feedback loop de testing instantáneo no es un lujo; es el único pilar que te permite iterar rápido sin destruir producción.

    El problema de Jest en proyectos modernos (Vite / ESM)

    Jest fue el rey indiscutible de las pruebas en JavaScript durante casi una década. Sin embargo, su arquitectura arrastra decisiones de diseño pasadas:

    • Utiliza transformadores pesados en Babel/ts-jest que recompilan todo tu código TypeScript en CommonJS antes de ejecutar cada test.
    • La emulación del DOM con jsdom consume gigabytes de memoria Heap en proyectos medianos.
    • La ejecución en Watch mode rehace trabajo redundante de empaquetado.

    Vitest aprovecha la arquitectura moderna de Vite y esm.sh: usa el mismo pipeline de transformación de tu aplicación en desarrollo, comparte la configuración de alias e importaciones y aprovecha hilos de ejecución paralelos en Node.js de forma nativa.

    Configuración de Vitest para Frontend e IA

    Instalar Vitest en un proyecto moderno requiere un archivo de configuración mínimo (vitest.config.ts):

    import { defineConfig } from 'vitest/config';
    
    export default defineConfig({
      test: {
        environment: 'happy-dom', // Mucho más rápido y ligero que jsdom
        globals: true,
        setupFiles: ['./src/test/setup.ts'],
        include: ['src/**/*.{test,spec}.ts'],
        coverage: {
          provider: 'v8',
          reporter: ['text', 'json', 'html'],
        },
      },
    });
    

    Cómo mockear llamadas a Modelos de IA (LLMs) sin gastar tokens

    Cuando pruebas código que interactúa con APIs de IA (como Anthropic Claude, OpenAI o Vercel AI SDK), jamás debes realizar peticiones HTTP reales dentro de tus tests unitarios. Lanzarías la factura de tokens por las nubes y harías que tus tests sean no deterministas.

    Ejemplo de Mockeo Limpio con vi.mock()

    import { describe, it, expect, vi } from 'vitest';
    import { procesarRespuestaIA } from './ai-processor';
    
    // 1. Mockear la librería cliente de IA
    vi.mock('@anthropic-ai/sdk', () => {
      return {
        Anthropic: vi.fn().mockImplementation(() => ({
          messages: {
            create: vi.fn().mockResolvedValue({
              content: [{ type: 'text', text: 'Respuesta mockeada de prueba' }],
            }),
          },
        })),
      };
    });
    
    describe('Procesador de IA', () => {
      it('debe transformar la respuesta de la IA en una estructura válida', async () => {
        const resultado = await procesarRespuestaIA('Prompt de prueba');
        
        expect(resultado.ok).toBe(true);
        expect(resultado.texto).toBe('Respuesta mockeada de prueba');
      });
    });
    

    Al aplicar programación defensiva en TypeScript, garantizas que tus mocks cumplan exactamente con los contratos de tipos de las librerías originales.

    3 Principios para Suites de Test Ultrarrápidas

    1. Usa happy-dom en lugar de jsdom: happy-dom implementa las APIs del navegador necesarias para componentes de UI ocupando un 70% menos de memoria y ejecutando tests hasta 3 veces más rápido.
    2. Aísla las capas de tu aplicación: Separa los tests de unidades puras (funciones de dominio y utilidades) de los tests de componentes de UI. Al igual que recomendamos en nuestra guía sobre graph engineering, mantener fronteras claras evita inicializaciones innecesarias.
    3. No intentes testearlo todo: En nuestro análisis sobre cuándo NO usar Spec-Driven Development, recordamos que el objetivo de las pruebas es dar confianza en cambios de producción, no alcanzar el 100% de cobertura en código trivial sin valor de negocio.

    La velocidad de ejecución de tus pruebas unitarias determina el ritmo al que tu equipo puede innovar y refactorizar con seguridad.

    Si quieres aprender a construir pipelines de testing modernos e integraciones con IA, descubre los Cursos de Dominicode. Y si quieres colaborar en proyectos reales de alto nivel con desarrolladores senior, súmate a Dominicode Labs.

    Preguntas frecuentes

    ¿Es dificil migrar una suite existente de Jest a Vitest?

    No. Vitest incluye compatibilidad casi al 100% con las APIs de Jest (describe, it, expect, jest.fn() -> vi.fn()). En la mayoría de los proyectos basta con sustituir la importación y la configuración de jest.config.js por vitest.config.ts.

    ¿Se pueden ejecutar tests de Vitest en modo Watch continuo durante el desarrollo?

    Sí. El modo Watch de Vitest es uno de sus puntos más fuertes: gracias al HMR de Vite, solo reejecuta en milisegundos los tests directamente afectados por el archivo que acabas de modificar en tu editor.

    ¿Cómo pruebo componentes que utilizan hooks o reactividad de framework?

    Vitest se integra perfectamente con @testing-library/react, @testing-library/angular o @testing-library/vue, permitiéndote probar la interacción del usuario con componentes de UI usando la misma sintaxis que ya conoces.

    ¿Vitest funciona en proyectos que no usan Vite como empaquetador principal?

    Sí. Aunque Vitest brilla especialmente en proyectos basados en Vite (como Nuxt, Astro, SvelteKit o React con Vite), puede configurarse y funcionar perfectamente en cualquier proyecto de Node.js o TypeScript independiente.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • La arquitectura moderna de Angular en 2026: Cómo los Signals y Signal Forms cambiaron el juego

    La arquitectura moderna de Angular en 2026: Cómo los Signals y Signal Forms cambiaron el juego

    Hace un par de años revisé una aplicación empresarial desarrollada en Angular 14. Tenía más de 80 componentes. Para sincronizar dos campos de un formulario y calcular un total dinámico, el equipo había montado un laberinto de BehaviorSubject, combineLatest, operadores de RxJS como distinctUntilChanged y tuberías async por todas partes.

    Cualquier cambio menor requería tocar 4 archivos distintos. Los desarrolladores pasaban más tiempo luchando contra fugas de memoria por suscripciones no canceladas que construyendo funcionalidades de negocio.

    Cuando refactorizamos esa misma aplicación a las versiones modernas de Angular basándonos en Signals y Signal Forms, eliminamos el 45% del boilerplate y la velocidad de renderizado en el navegador se disparó.

    Angular ha cambiado radicalmente. Si sigues programando en Angular como lo hacías en 2020, te estás perdiendo la mayor revolución de usabilidad en la historia del framework.

    El problema histórico: RxJS en la capa de UI

    RxJS es una librería extraordinaria para manejar eventos asíncronos complejos como websockets, streams de datos o cancelación de peticiones HTTP.

    Sin embargo, usar RxJS para gestionar el estado reactivo simple dentro de un componente de UI era una mala decisión forzada por las limitaciones del framework anterior:

    • Tenías que lidiar con la subscripción y desuscripción explícita (takeUntilDestroyed, async pipe).
    • Zone.js tenía que comprobar el árbol completo de componentes ante cualquier evento, provocando ciclos de detección de cambios innecesarios.
    • La sintaxis resultaba verbosa y confusa para desarrolladores que venían de otros ecosistemas.

    Con el rápido ciclo de releases de Angular, el equipo del framework ha introducido un modelo reactivo síncrono, directo y ultrarrápido: los Signals.

    El nuevo paradigma: Grafo Reactivo con Signals

    En la arquitectura moderna de Angular, la reactividad síncrona dentro del componente se gestiona mediante tres primitivas fundamentales:

    1. signal() (Estado primario)

    Representa un valor reactivo mutable. Al modificar su valor, Angular sabe exactamente qué nodo del DOM depende de él y actualiza únicamente ese elemento.

    const cantidad = signal<number>(1);
    const precioUnitario = signal<number>(25);
    

    2. computed() (Estado derivado)

    Calcula automáticamente un nuevo valor en función de otros signals. Es perezoso (lazy) y memoriza su resultado (memoized), por lo que solo se reevalúa cuando uno de sus signals dependientes cambia.

    const total = computed(() => cantidad() * precioUnitario());
    

    3. effect() (Efectos secundarios)

    Se ejecuta cuando los signals leídos en su interior cambian.

    • Regla de oro de arquitectura: Jamás uses effect() para modificar otro signal o para lógica de negocio. Reservalo exclusivamente para sincronización externa (ej. guardar en localStorage, emitir eventos a librerías de terceros o analytics).

    Como recalcamos en nuestras guías de programación defensiva en TypeScript, tipar de forma estricta los valores de tus signals previene errores de estado en tiempo de ejecución.

    Signal Forms y Zoneless: El salto definitivo

    Dos de las piezas más esperadas en el ecosistema moderno son la integración de Signal Forms y el soporte nativo para aplicaciones Zoneless (sin Zone.js).

    Zoneless Angular

    Al basar la UI en Signals, Angular ya no necesita Zone.js para "mono-patrullar" los eventos del navegador (clicks, timers, peticiones HTTP). El framework sabe con precisión quirúrgica qué componente y qué nodo del DOM ha cambiado. El resultado es un consumo de memoria mínimo y un arranque instantáneo.

    El rol actual de RxJS

    RxJS no ha muerto ni va a desaparecer. La regla de arquitectura actual es sencilla:

    • Estado de UI y componentes: Usa 100% Signals (signal, computed, input, output).
    • Eventos asíncronos complejos y HTTP: Usa RxJS (HttpClient, debounceTime, switchMap) y conviértelo a Signal en la frontera del componente mediante toSignal().
    // Integración perfecta: RxJS hacia la API, Signal hacia la plantilla
    readonly usuarios = toSignal(this.userService.getUsuarios(), { initialValue: [] });
    

    La arquitectura moderna de Angular es más limpia, más rápida de aprender y infinitamente más fácil de mantener que en versiones anteriores.

    Si quieres dominar el desarrollo frontend moderno y la integración con herramientas de IA, descubre los Cursos de Dominicode. Y si quieres aplicar estas arquitecturas en proyectos reales de producción, te invitamos a sumarte a Dominicode Labs.

    Preguntas frecuentes

    ¿Debo migrar todos mis BehaviorSubject a Signals inmediatamente?

    No es necesario hacer una migración destructiva de golpe. Puedes mantener tu capa de servicios en RxJS e ir adoptando toSignal() y signal() en la capa de componentes de forma progresiva.

    ¿Cuándo debo usar RxJS en lugar de Signals?

    Utiliza RxJS cuando necesites coordinar múltiples eventos asíncronos en el tiempo (por ejemplo, autocompletados con debounceTime, polling periódico, cancelación de peticiones anteriores con switchMap o gestión de WebSockets).

    ¿Cómo afecta el uso de Signals al SEO y SSR en Angular?

    Los Signals mejoran el rendimiento de Server-Side Rendering (SSR) y Hydration ya que reducen la sobrecarga de evaluación de cambios en el servidor y permiten una hidratación parcial ultrarrápida en el cliente.

    ¿Qué versión de Angular se recomienda para trabajar con Signals de forma estable?

    Signals se introdujo como developer preview en Angular 16 y alcanzó estabilidad en Angular 17/18. Para contar con APIs maduras como input(), output(), model() y Signal Forms se recomienda usar Angular 19/20+.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Clean Architecture en Frontend: Cómo estructurar tus aplicaciones para sobrevivir a la era de la IA

    Clean Architecture en Frontend: Cómo estructurar tus aplicaciones para sobrevivir a la era de la IA

    Hace unos meses audité una aplicación de un cliente que tenía más de 50 componentes. Cuando abrí el archivo de un simple formulario de checkout, me encontré con 750 líneas de código: llamadas directas a fetch, manipulación de tokens de autenticación, formateo de fechas, cálculo de impuestos y renderizado visual de botones. Todo apretado en el mismo sitio.

    El cliente me decía frustrado: "Intentamos usar agentes de IA para añadir un nuevo método de pago y el agente rompe la aplicación entera cada vez".

    El problema no era la herramienta de IA. El problema es que los modelos de lenguaje necesitan fronteras claras para no perderse. Cuando mezclas UI, estado y lógica de negocio en una bola de barro, obligas a la IA a interpretar miles de líneas irrelevantes para hacer un cambio trivial.

    Clean Architecture en frontend no es un capricho teórico. Es la única forma de construir aplicaciones mantenibles que tanto los humanos como los agentes de IA puedan modificar sin romper producción.

    El problema del espagueti en la capa de presentación

    Durante años nos enseñaron que organizar una app frontend consistía en crear carpetas como /components, /services y /utils.

    Esa estructura por "tipo de archivo" suele degenerar en componentes gigantescos que contienen:

    • Lógica de UI (animaciones, estados de modales, hovers).
    • Lógica de Negocio (validación de carritos, cálculo de descuentos, reglas de usuario).
    • Lógica de Infraestructura (llamadas a la API HTTP, almacenamiento en localStorage).

    Cuando un agente de IA intenta refactorizar o añadir una funcionalidad a un componente así, el resultado son alucinaciones, código duplicado y efectos secundarios inesperados. Como demostramos en nuestro post sobre programación defensiva en TypeScript, la falta de contratos claros es la causa principal de fallos silenciosos.

    Las 3 Capas de Clean Architecture en Frontend

    Para que una aplicación frontend sea desacoplada y amigable para el desarrollo asistido por IA, debemos dividir el proyecto en tres capas concéntricas con reglas de dependencia estrictas:

    ┌─────────────────────────────────────────────────────────┐
    │ Presentación (React / Angular / Vue Components)         │
    │  └─► Llaman a Casos de Uso                             │
    │     ┌───────────────────────────────────────────────────┐
    │     │ Dominio (Entities, Use Cases, Interfaces Repos)   │
    │     │  └─► Cero dependencias externas o de UI           │
    │     └───────────────────────────────────────────────────┘
    │  ┌──────────────────────────────────────────────────────┐
    │  │ Infraestructura (HTTP Repositories, LocalStorage)    │
    │  │  └─► Implementan las Interfaces del Dominio          │
    │  └──────────────────────────────────────────────────────┘
    └─────────────────────────────────────────────────────────┘
    

    1. Capa de Dominio (El Corazón de tu App)

    Contiene las Entidades y los Casos de Uso pura lógica de TypeScript.

    • Regla de oro: No importa qué framework estés usando. La capa de dominio NO debe importar nada de React, Angular, Vue o librerías HTTP.
    • Ejemplo: CalcularDescuentoUseCase, UsuarioEntity, CarritoInterface.

    2. Capa de Infraestructura (Conexiones Externas)

    Implementa las interfaces definidas por el dominio para interactuar con APIs externas, bases de datos o servicios del navegador.

    • Ejemplo: UserHttpRepository que implementa UserRepository, clientes Axios/Fetch, adaptores de localStorage.

    3. Capa de Presentación (Vista e Interacción)

    Se limita a pintar los datos y capturar eventos del usuario. Sus componentes invocan los Casos de Uso y reaccionan al estado presentado.

    • Ejemplo: Componentes visuales, señales/hooks de estado UI, botones, maquetación.

    Por qué esta arquitectura multiplica la velocidad de la IA

    Cuando tu aplicación sigue Clean Architecture, trabajar con asistentes como Claude Code o Cursor se vuelve ridículamente eficiente:

    1. Prompting aislado: Si necesitas cambiar una regla de negocio (ej. "los clientes VIP tienen un 15% de descuento en lugar del 10%"), solo le pides a la IA que modifique el archivo CalcularDescuentoUseCase.ts. La IA no toca ni un solo archivo de UI.
    2. Generación automática de Tests: Probar un Caso de Uso puro de TypeScript no requiere renderizar componentes ni simular el DOM (jsdom/happy-dom). La IA puede escribir y validar 20 unit tests puramente lógicos en 5 segundos.
    3. Sustitución de UI sin riesgo: Puedes pedirle a un agente de IA que rediseñe un componente visual entero desde cero, sabiendo que la lógica de negocio subyacente permanecerá intacta.

    Como explicamos al analizar el graph engineering, proporcionarle a la IA un mapa claro de dependencias previene que introduzca acoplamientos indeseados.

    Y recuerda: aunque Clean Architecture aporta enormes beneficios en apps de tamaño mediano y grande, en nuestra guía sobre cuándo NO usar Spec-Driven Development analizamos los casos de uso donde soluciones más simples resultan más recomendables.


    Separar las responsabilidades de tu código no es solo una buena práctica de ingeniería; es la mejor inversión para escalar aplicaciones en la era de los agentes autónomos.

    Si quieres aprender a diseñar arquitecturas robustas y escalables desde cero, échale un vistazo a los Cursos de Dominicode. Y si quieres construir proyectos complejos en un entorno colaborativo de alto nivel, te esperamos en Dominicode Labs.

    Preguntas frecuentes

    ¿No añade Clean Architecture demasiada sobrecarga de archivos en proyectos pequeños?

    Para proyectos tipo "landing page" o prototipos simples de pocas semanas, Clean Architecture puede resultar excesiva. Sin embargo, para aplicaciones que van a vivir en producción durante años o mantenidas por equipos, el ahorro en mantenimiento compensa con creces la estructura inicial.

    ¿Dónde encaja la gestión de estado (Redux, NgRx, Zustand, Signals)?

    La gestión de estado vive en la capa de Presentación/UI. Los stores o señales consumen los Casos de Uso del Dominio y exponen el estado procesado a los componentes visuales.

    ¿Cómo interactúa el Dominio con las llamadas a la API sin importar HTTP Client?

    El Dominio define una interfaz abstracta (ej. export interface UserRepository { getById(id: string): Promise<User>; }). La Capa de Infraestructura implementa esa interfaz con llamadas HTTP reales mediante inyección de dependencias.

    ¿Por qué los agentes de IA entienden mejor la Clean Architecture?

    Porque los límites de responsabilidad están definidos a nivel de carpetas y contratos. La IA no tiene que adivinar dónde termina la lógica de interfaz y dónde empieza la validación de negocio.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Patrones de diseño avanzados en TypeScript para aplicaciones en producción real

    Patrones de diseño avanzados en TypeScript para aplicaciones en producción real

    Hace unos meses revisé el repositorio de un proyecto TypeScript con más de dos años en producción. Al abrir el archivo de configuración del cliente principal, encontré comentarios como // TODO: quitar este any y casteos del tipo const data = response as unknown as UserData desperdigados por todo el código.

    El equipo se quejaba de que el compilador de TypeScript "les molestaba" en lugar de ayudarles.

    El problema era que estaban usando TypeScript simplemente como "JavaScript con anotaciones de tipo superficiales". No estaban aprovechando el sistema de tipos algebraicos ni los patrones de diseño expresivos que convierten a TypeScript en uno de los lenguajes más potentes para construir software resiliente.

    Conocer patrones de diseño avanzados en TypeScript no es para aprobar una entrevista técnica. Es lo que separa el código frágil del código mantenible que soporta años de evolución en producción.

    El espejismo del casting as Type

    El primer síntoma de una base de código TypeScript débil es el uso indiscriminado de aserciones de tipo (as).

    Cuando escribes as UserData, le estás diciendo al compilador: "Cállate, yo sé más que tú". Le estás quitando a TypeScript su superpoder principal: garantizar en tiempo de compilación que tus estructuras de datos son válidas.

    Como ya explicamos en nuestro análisis sobre programación defensiva en TypeScript, forzar tipos sin validación es la receta perfecta para lanzar excepciones TypeError: Cannot read properties of undefined en medio de la noche.

    3 Patrones de Diseño Esenciales para Developers Senior

    1. El Patrón Result (Discriminated Unions para manejo de errores)

    En lugar de lanzar excepciones con throw (que son invisibles en la firma de tus funciones), utiliza un tipo Result explícito basado en uniones discriminadas:

    type Success<T> = { readonly ok: true; readonly value: T };
    type Failure<E> = { readonly ok: false; readonly error: E };
    type Result<T, E = Error> = Success<T> | Failure<E>;
    
    function parseConfig(rawJson: string): Result<AppConfig, ParseError> {
      try {
        const data = JSON.parse(rawJson);
        if (!data.apiKey) {
          return { ok: false, error: new ParseError("apiKey es requerida") };
        }
        return { ok: true, value: data as AppConfig };
      } catch (e) {
        return { ok: false, error: new ParseError("JSON inválido") };
      }
    }
    
    // Uso obligatorio y seguro:
    const result = parseConfig(rawString);
    if (result.ok) {
      console.log(result.value.apiKey); // TypeScript sabe que ok es true y infiere el tipo de 'value'
    } else {
      console.error(result.error.message); // TypeScript infiere el tipo de 'error'
    }
    

    2. El Patrón Strategy para Proveedores de IA y Servicios

    Cuando construyes aplicaciones que interactúan con múltiples modelos de IA (OpenAI, Anthropic Claude, Ollama en local), el patrón Strategy te permite intercambiar algoritmos y proveedores sin modificar el código cliente:

    interface AIProviderStrategy {
      readonly name: string;
      generateCompletion(prompt: string): Promise<string>;
    }
    
    class AnthropicStrategy implements AIProviderStrategy {
      readonly name = "anthropic";
      async generateCompletion(prompt: string): Promise<string> {
        // Lógica específica para Anthropic API
        return "Respuesta de Claude";
      }
    }
    
    class OllamaLocalStrategy implements AIProviderStrategy {
      readonly name = "ollama";
      async generateCompletion(prompt: string): Promise<string> {
        // Lógica específica para modelo en local
        return "Respuesta de LLM local";
      }
    }
    
    class AIService {
      constructor(private strategy: AIProviderStrategy) {}
    
      setStrategy(newStrategy: AIProviderStrategy) {
        this.strategy = newStrategy;
      }
    
      async run(prompt: string) {
        return await this.strategy.generateCompletion(prompt);
      }
    }
    

    3. Builder Pattern con Validación en Tiempo de Compilación

    El patrón Builder permite crear objetos complejos garantizando que todos los parámetros requeridos se hayan establecido antes de instanciar la clase:

    type CompleteState = { host: string; port: number };
    
    class DatabaseConfigBuilder<State extends Partial<CompleteState> = {}> {
      private constructor(private readonly config: Partial<CompleteState>) {}
    
      static create(): DatabaseConfigBuilder<{}> {
        return new DatabaseConfigBuilder({});
      }
    
      setHost(host: string): DatabaseConfigBuilder<State & { host: string }> {
        return new DatabaseConfigBuilder({ ...this.config, host });
      }
    
      setPort(port: number): DatabaseConfigBuilder<State & { port: number }> {
        return new DatabaseConfigBuilder({ ...this.config, port });
      }
    
      build(this: DatabaseConfigBuilder<CompleteState>): DatabaseConfig {
        return new DatabaseConfig(this.config.host, this.config.port);
      }
    }
    

    Si intentas llamar a .build() sin haber establecido setHost() y setPort(), TypeScript rechazará la compilación de forma inmediata.


    Diseñar aplicaciones en TypeScript utilizando patrones expresivos y tipos algebraicos previene el 90% de los errores en producción antes de que el código toque el servidor.

    Al igual que discutimos al construir agentes de voz en tiempo real con TypeScript, el rigor en el tipado y en los contratos es lo que permite que tu código escale sin romperse. Y si combinas estos patrones con técnicas de graph engineering, tus aplicaciones serán limpias, modulares e inexpugnables.

    Si deseas dominar TypeScript avanzado y patrones de arquitectura modernos, explora los Cursos de Dominicode. Y si quieres construir software de producción en comunidad con otros desarrolladores senior, te esperamos en Dominicode Labs.

    Preguntas frecuentes

    ¿Cuándo debo preferir type sobre interface en TypeScript?

    Como regla general: usa interface cuando estés definiendo contratos de objetos que pueden ser extendidos o implementados por clases (OOP). Usa type para uniones, tuplas, tipos primitivos mapeados y combinaciones algebraicas de tipos.

    ¿El uso de tipos avanzados degrada el rendimiento de la aplicación en producción?

    No. Todo el sistema de tipos de TypeScript se elimina por completo durante el proceso de transpilación a JavaScript. Tu bundle de producción solo contiene JavaScript puro, por lo que los tipos no agregan ni un solo byte de sobrecarga en tiempo de ejecución.

    ¿El patrón Result reemplaza por completo los bloques try/catch?

    Se recomienda usar el patrón Result para errores de dominio previsibles (fallos de validación, usuario no encontrado, saldo insuficiente). Los bloques try/catch se reservan para excepciones no controladas a nivel de infraestructura (caída de red, falta de memoria).

    ¿Por qué evitar el tipo any si a veces acelera el desarrollo?

    Usar any desactiva completamente el verificador de tipos de TypeScript para esa variable y para todas las expresiones derivadas de ella. Si necesitas un tipo genérico desconocido temporalmente, utiliza siempre unknown y realiza narrowing con guardas de tipo (type guards).


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.