Category: JavaScript

  • Construyendo Agentes Rápidos con TypeScript y Vercel AI SDK

    Construyendo Agentes Rápidos con TypeScript y Vercel AI SDK

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido

    Tiempo estimado de lectura: 4 min

    • Tipado + validación: TypeScript en la superficie y Zod en runtime reducen errores silenciosos y permiten refactors seguros.
    • API unificada: Vercel AI SDK conecta proveedores y ofrece streaming y herramientas tipadas.
    • Extracción y control: generateObject y esquemas evitan ingeniería de prompt frágil y JSON truncado.
    • UX y operaciones: streamText mejora la percepción de latencia; métricas y circuit breakers mantienen robustez en producción.

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido. Si vas a poner agentes en producción, necesitas que la capa que conecta al LLM con tus herramientas sea predecible, tipada y validada desde el primer día. Esa combinación reduce errores silenciosos, acelera refactors y convierte promesas estocásticas en contratos verificables.

    Resumen rápido (lectores con prisa)

    TypeScript para tipado estático, Zod para validación en runtime y Vercel AI SDK como API unificada. Juntos: herramientas tipadas, extracción estructurada (generateObject), y streaming (streamText) para agentes más seguros y previsibles.

    TypeScript + Vercel AI SDK: por qué funciona para agentes rápidos

    Tres problemas recurrentes al construir agentes:

    1. El LLM alucina parámetros para las herramientas (tool calls)

    Los modelos pueden generar parámetros inválidos o inventados para llamadas a herramientas, lo que puede llevar a ejecuciones peligrosas si no se validan antes.

    2. Las respuestas JSON vienen envueltas en markdown o truncadas

    Solemos ver JSON con backticks, texto adicional o respuestas incompletas que complican el parsing confiable.

    3. Cambios en la API del proveedor rompen integraciones silenciosamente

    Actualizar modelos o proveedores puede introducir cambios incompatibles si no hay contratos y pruebas robustas.

    La solución práctica es simple: tipos en la superficie (TypeScript), contratos ejecutables (Zod) y una API que integra ambas cosas (Vercel AI SDK). Beneficios concretos:

    • Autocompletado que evita buscar docs.
    • Tool calls que no se ejecutan si los datos no validan.
    • Extracción de objetos estructurados (generateObject) sin ingeniería de prompt frágil.
    • Streaming nativo (streamText) para UX reactiva.

    Tool calls tipados: la barrera que evita ejecuciones peligrosas

    Definir herramientas con esquemas evita que el agente ejecute acciones con parámetros inventados. Ejemplo:

    import { tool } from 'ai';
    import { z } from 'zod';
    
    const searchOrders = tool({
      description: 'Busca pedidos por ID de cliente',
      parameters: z.object({
        customerId: z.string().uuid(),
        status: z.enum(['pending','shipped','delivered']).optional(),
      }),
      execute: async ({ customerId, status }) => {
        return queryOrdersDatabase({ customerId, status });
      },
    });
    

    Si el LLM devuelve un customerId inválido, Zod lo rechazará antes de llamar a execute. Resultado: menos excepciones en la base de datos y trazabilidad clara del fallo (prompt → validación → rechazo).

    generateObject: extracción fiable de datos estructurados

    generateObject obliga al modelo a respetar un esquema y te devuelve un objeto tipado sin hacer JSON.parse() manual. Ejemplo práctico:

    import { generateObject } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import { z } from 'zod';
    
    const schema = z.object({
      sentiment: z.enum(['positive','neutral','negative']),
      confidence: z.number().min(0).max(1),
      topics: z.array(z.string()).max(5)
    });
    
    const { object } = await generateObject({
      model: openai('gpt-4o'),
      schema,
      prompt: 'Analiza la reseña y devuelve sentiment, confidence y topics.'
    });
    
    // object ya está tipado según schema
    

    Esto reduce la ingeniería de prompts (“Devuelve SOLO JSON”) y aumenta la tasa de respuestas utilizables desde el primer intento.

    streamText: UX que comunica progreso y permite pasos intermedios

    Los agentes suelen ejecutar varias herramientas en cadena. streamText permite emitir texto progresivo y reflejar estados intermedios (p. ej. “consultando base de datos…”) en la UI sin arquitectura adicional:

    • Emite tokens progresivamente al frontend.
    • Reporta eventos de invocation/execute de herramientas.
    • Funciona tanto en Server (Next.js) como en cliente con hooks (useChat).

    Esto mejora la percepción de latencia y permite interacciones más naturales con agentes multi‑paso.

    Integración práctica y operaciones en producción

    Patrón recomendado

    1. Diseña esquemas Zod como fuente única de verdad.
    2. Expón el esquema (o ejemplo) en el prompt para guiar al LLM.
    3. Usa safeParse() para reintentos y autocorrección de prompts; usa parse() para endpoints que deben fallar rápido.
    4. Loguea prompt, raw response y error de Zod (flatten) para trazabilidad.

    Medidas operativas

    • Métricas: tasa de validación fallida, latencia media por herramienta, reintentos por prompt.
    • Retries limitados con backoff y contador de intentos (p. ej. 2 reintentos de autocorrección antes de degradar a humano).
    • Circuit breaker para evitar invocar herramientas costosas si la validación falla en cascada.

    Limitaciones y decisions trade‑offs

    • No eliminas la estocasticidad del LLM; la controlas. Algunos casos requerirán supervisión humana.
    • generateObject y Structured Outputs reducen errores de formato, pero no sustituyen la validación semántica (p. ej. números positivos). Zod sigue siendo necesaria.
    • Tipar desde el día 0 impone disciplina, pero acelera onboarding y refactors.

    Conclusión

    TypeScript + Vercel AI SDK: la combinación que uso para construir agentes rápido no es un truco de marketing. Es una estrategia concreta: tipos para detectar cambios, Zod para validar en runtime, y un SDK que une proveedores, streaming y herramientas tipadas. Si tu objetivo es desplegar agentes que actúen sobre sistemas reales—bases de datos, pedidos, o infraestructuras—esta pila reduce fallos silenciosos y convierte iteración rápida en ingeniería sostenible.

    Para equipos que exploran automatización y agentes como flujo de trabajo productivo, una guía práctica y recursos adicionales están disponibles en Dominicode Labs. Es una continuación lógica para quienes quieren aterrizar estas prácticas en sistemas reales.

    FAQ

    ¿Por qué combinar TypeScript con Zod y un SDK como Vercel AI SDK?

    TypeScript aporta seguridad estática y autocompletado; Zod proporciona validación en runtime; y Vercel AI SDK unifica la interacción con proveedores, streaming y herramientas tipadas. La combinación reduce errores silenciosos y facilita refactors.

    ¿Cómo evitan las herramientas tipadas ejecuciones peligrosas?

    Al definir parámetros con esquemas Zod, cualquier dato que no valide se rechaza antes de ejecutar la función execute, evitando operaciones con parámetros inventados o inválidos.

    ¿Qué ventaja ofrece generateObject frente a parsear JSON manualmente?

    generateObject obliga al modelo a respetar un esquema y devuelve un objeto ya tipado, evitando la ingeniería de prompt para forzar JSON y reduciendo errores por markdown, texto adicional o truncado.

    ¿Cuándo debo usar streamText?

    Cuando quieras mejorar la UX en interacciones multi‑paso: emitir tokens progresivamente, mostrar estados intermedios y reportar eventos de invocation/execute sin añadir complejidad arquitectónica.

    ¿Qué métricas operativas son críticas?

    Métricas como tasa de validación fallida, latencia media por herramienta y reintentos por prompt son esenciales para monitorear la salud y eficacia del agente.

    ¿Cuáles son las limitaciones principales de esta pila?

    No elimina la estocasticidad del LLM; solo la controla. También requiere validación semántica adicional (p. ej. asegurar números positivos). Tipar desde el día 0 impone disciplina, aunque acelera onboarding y refactors.

  • Errores comunes al migrar a React Server Components en producción

    Errores comunes al migrar a React Server Components en producción

    React Server Components en producción: errores que nadie te cuenta

    Tiempo estimado de lectura: 5 min

    • Fronteras claras: mezclar datos pesados del servidor con Client Components provoca serialización y payloads enormes.
    • No convertir todo a client: usar “use client” globalmente anula los beneficios de RSC y regresa a una SPA pesada.
    • Latencia y caching: llamadas secuenciales y caché agresiva generan TTFB alto y fugas de datos entre usuarios.
    • Audita dependencias: muchas librerías no están preparadas para ejecución en server; lazy-load o wrappers client son necesarios.

    Introducción

    React Server Components en producción: errores que nadie te cuenta. Lo digo sin rodeos: los tutoriales y demos no te preparan para operarlos en tráfico real. En ese salto es donde aparecen fugas de datos, payloads monstruosos y cuellos de botella invisibles que desarman la promesa de “menos JS, mejor rendimiento”.

    Este artículo enumera los fallos concretos que verás en proyectos reales, aporta soluciones técnicas y define cuándo NO migrar a RSC. Incluye referencias y enlaces oficiales para que puedas profundizar: Suspense y caching en Next.js.

    Resumen rápido (lectores con prisa)

    Qué es: Patrón que permite renderizar parte de la UI en el servidor y enviar un árbol serializado al cliente.

    Cuándo usarlo: cuando puedas controlar la frontera server/client, minimizar datos pasados al cliente y beneficiarte de menos JS inicial.

    Por qué importa: mejora rendimiento y seguridad si se adopta con disciplina en serialización, caché y orquestación de datos.

    Cómo funciona: Server Components pueden acceder a recursos de servidor; Client Components se hidratan en cliente y deben recibir solo datos mínimos.

    1. React Server Components en producción: la frontera que rompe todo

    El error raíz es conceptual: tratar la frontera server/client como una línea estética en lugar de una decisión arquitectónica. Un Server Component puede acceder a la BD y luego pasar objetos enormes como props a un Client Component. Eso obliga a React a serializar todo en el HTML/JSON de respuesta. Resultado: la reducción del bundle se convierte en megabytes de payload.

    Ejemplo típico (malo)

    • Server Component hace SELECT * FROM orders WHERE user_id = ? y pasa todos los registros a <OrdersTable use client />.
    • El navegador recibe un payload serializado de decenas de MB.

    Solución: procesar, paginar y resumir en el servidor. Pasa al cliente solo el minimum viable (IDs, count, primeros N items) y provee endpoints client-side para cargar la página de datos al interactuar.

    2. El pánico del “use client” y la regresión a SPA

    Cuando algo falla (proveedores, librerías de UI, hooks), el atajo más común es colocar "use client" en el layout. Eso convierte todo el árbol en Client Components y anula el beneficio de RSC: vuelves a una SPA grande, con mayor complejidad y sin reducción de JS.

    Patrón correcto:

    • Mantén providers y estado en componentes hoja que realmente necesitan interactividad.
    • Diseña la composición para que los Client Components reciban props mínimos y, si requieren datos pesados, llamen a endpoints específicos (fetch desde cliente) o utilicen streaming.

    3. Waterfalls invisibles: el backend secuencial que mata TTFB

    Código like-this en Server Component:

    const user = await getUser(id);
    const prefs = await getPrefs(user.configId);
    const orders = await getOrders(user.id);
    

    Eso es secuencial: suma latencias. Aunque ocurre en servidor, el usuario espera. Paraleleza con Promise.all cuando no hay dependencia, y usa Suspense para streaming progresivo cuando sí hay dependencias parciales.

    Patrón secuencial y solución

    • Identifica llamadas independientes y ejecútalas en paralelo.
    • Usa streaming y Suspense para mostrar partes de la vista cuando están listas.
    • Mide TTFB en staging bajo carga para detectar waterfalls invisibles.

    4. Caché agresiva = fuga de datos entre usuarios

    Next.js y otros frameworks aplican caching por defecto en render server. Si renderizas una ruta con datos privados y no marcas la petición como dinámica, puedes cachear la vista de un usuario y servirla a otro. Es real y está pasando en producción.

    Contramedidas:

    • Para datos privados usa { cache: 'no-store' } en fetch o llama a APIs que leen cookies()/headers() (esto fuerza render dinámico en Next.js).
    • Revisa la documentación de caché de Next.js: caching en Next.js.
    • Considera políticas CDN más conservadoras para rutas autenticadas.

    5. Integraciones de terceros que no están listas para server execution

    Muchas librerías npm asumen un entorno DOM. Al ejecutar en server, aparecen errores en build o comportamiento inesperado. Resultado: el equipo marca "use client" masivo y pierde las ventajas. Revisa dependencias: algunas requieren reemplazo o lazy-loading estricto.

    Táctica práctica:

    • Audita las dependencias con npm ls y pruebas de build en CI que marquen dónde fallan.
    • Si una librería solo se usa en un widget, envuélvela en un Client Component lazy-loaded.

    Cuándo NO usar React Server Components

    No migres a RSC si tu producto encaja en alguno de estos casos:

    • Aplicaciones offline-first o PWAs que deben funcionar sin servidor.
    • Interfaces de hiper-interactividad: editores gráficos, juegos, vídeo en tiempo real o UIs con WebSockets a alta frecuencia.
    • Bases de código legacy sin presupuesto de reescritura: migrar Redux heavy/class components = reescritura, no refactor.

    Checklist práctico antes de migrar a producción

    1. Delimita claramente boundaries: quién corre en server y qué mínima data pasa al cliente.
    2. Añade tests de integración que simulen carga y validen payloads.
    3. Forza políticas de cache por ruta (privada vs pública).
    4. Instrumenta logs de tamaño de respuesta y tokenización/serialización.
    5. Adopta streaming/Suspense para vistas complejas; usa Promise.all para llamadas paralelas.
    6. Audita dependencias y evita convertir el layout en client por comodidad.

    Conclusión: RSC exige disciplina, no solo adopción

    React Server Components entregan ventajas claras (menos JS inicial, mayor seguridad para secretos, mejor SEO). Pero funcionan en producción solo si el equipo re-aprende backend: serialización, caching, latencia y orquestación de datos. La migración exitosa no es técnica aislada; es un cambio de modelo mental: pasar de “componentes” a “árboles de dependencias de red”. Si no estás dispuesto a trazar fronteras con rigor, no migres: estarás complicando tu arquitectura sin ganar sus beneficios.

    FAQ

    ¿Qué es exactamente un React Server Component?

    Un React Server Component se renderiza en el servidor y puede acceder a recursos del backend. No se hidrata en el cliente como un Client Component y se envía serializado al navegador.

    ¿Cuándo debo evitar migrar a RSC?

    Evita migrar si necesitas soporte offline completo, tienes UIs de hiper-interactividad (editores, juegos, video en tiempo real) o una base de código legacy sin presupuesto para reescritura.

    ¿Cómo evito pasar payloads gigantes al cliente?

    No pases objetos completos como props. Resumir, paginar y enviar solo lo mínimo necesario (IDs, count, primeros N items). Usa endpoints client-side para cargar datos adicionales bajo demanda.

    ¿Qué problemas de caché debo vigilar en Next.js?

    Cuidado con el render estático por defecto: rutas con datos privados pueden quedar cacheadas. Para datos privados usa { cache: 'no-store' } en fetch o APIs que lean cookies()/headers() para forzar render dinámico.

    ¿Cómo detectar y arreglar waterfalls en Server Components?

    Mide TTFB en staging bajo carga, revisa llamadas secuenciales en Server Components y paraleliza con Promise.all cuando sea posible. Usa streaming y Suspense para render progresivo.

    ¿Qué hago con librerías que fallan en server?

    Audita dependencias con npm ls, añade pruebas de build en CI y envuelve las librerías problemáticas en Client Components lazy-loaded o busca alternativas compatibles con server execution.

  • Cómo elegir entre Hono, NestJS y Express en 2026

    Cómo elegir entre Hono, NestJS y Express en 2026

    Hono vs Express vs NestJS: cuál usar en 2026

    Tiempo estimado de lectura: 4 min

    Ideas clave

    • Decide por ejecución y mantenimiento: Edge/Serverless → Hono; contenedores y equipos grandes → NestJS; Express solo para legacy.
    • Hono sigue estándares Web (Fetch API) → despliegue directo en Cloudflare Workers, Deno y Bun; bundles mínimos y cold starts bajos.
    • NestJS aporta estructura, DI y patterns enterprise que facilitan gobernanza en equipos grandes.
    • Patrón recomendado: combinar Hono en el perímetro y NestJS en el core para optimizar latencia y mantenibilidad.

    Tabla de contenidos

    Resumen rápido (lectores con prisa)

    Hono es un microframework basado en la Fetch API ideal para despliegues Edge/Serverless con cold starts mínimos. NestJS es un framework estructurado con DI pensado para equipos grandes y aplicaciones empresariales. Express queda como opción legacy; para nuevo desarrollo en Node considera Fastify. Usa Hono en el perímetro y NestJS en el core cuando necesitas ambas propiedades.

    Introducción

    Hono vs Express vs NestJS: cuál usar en 2026 es la pregunta que debes resolver antes del primer commit. No es trivia: elegir mal condiciona latencia, coste, pruebas y, sobre todo, la capacidad del equipo para mantener código sano durante años.

    Si vas a desplegar en Cloudflare Workers, Deno o Bun, Hono cambia las reglas. Si tu sistema vive en Kubernetes y lo mantienen varios equipos, NestJS también cambia las reglas. Express… debería quedarse en mantenimiento.

    Hono vs Express vs NestJS: criterio de elección en 2026

    Primera regla: responde a dos preguntas concretas antes de elegir.

    • ¿Dónde se ejecutará el código? (Edge / Serverless vs contenedor)
    • ¿Quién lo mantendrá? (1–3 devs vs equipos >5)

    Hono está diseñado sobre estándares Web (Fetch API). Eso le da dos ventajas técnicas inmediatas: ejecuta sin cambios en Cloudflare Workers y Deno, y los bundles son mínimos → cold starts casi nulos. Ideal para servicios perimetrales: autenticación perimetral, proxies, transformaciones ligeras y APIs públicas de alta concurrencia.

    NestJS es lo opuesto deliberado: peso inicial mayor, pero estructura y DI que escalan en equipos grandes. Si necesitas módulos, interceptores, pipes, testing con mocks y un modelo mental homogéneo entre 10–50 ingenieros, NestJS reduce la deuda humana.

    Express sigue vivo por legacy. Técnicamente tiene problemas: acoplamiento a primitivas de Node (IncomingMessage/ServerResponse), soporte TypeScript no nativo y mala compatibilidad con runtimes Edge sin polyfills. Para proyectos nuevos, Fastify es una alternativa Node que merece consideración por rendimiento y plugins modernos.

    Performance y cold starts: cuándo importa realmente

    Si tu SLA pide latencia global baja y tus endpoints reciben picos distribuidos geográficamente, los cold starts importan. Hono arranca en milisegundos; NestJS arranca en cientos. Esa diferencia se traduce en UX y factura cuando se escala en funciones serverless.

    Ejemplo práctico:

    • API pública de alta concurrencia (CDN + Edge): Hono en Workers.
    • Sistema de facturación con colas, auditoría y scheduling: NestJS en contenedores.

    No mezcles requisitos. Si necesitas ambos, separa responsabilidades: Hono en el perímetro, NestJS en el núcleo.

    Escalabilidad de equipo y mantenibilidad

    NestJS gana por goleada en proyectos donde:

    • Múltiples equipos aportan features.
    • Necesitas contratos claros (interfaces, DTOs, guards).
    • Quieres testing coherente con DI y mocks.

    Hono exige disciplina. Sin una capa organizativa —convenios de carpeta, inyección manual, testing— terminas con endpoints inconexos. Aún así, para equipos pequeños o equipos senior que aceptan convenciones internas, Hono es una opción mantenible.

    Seguridad, observabilidad y ecosistema

    NestJS integra patterns enterprise: interceptores para logging, guards para auth, módulos para integración de colas (BullMQ), microservicios gRPC, etc. Si necesitas trazabilidad distribuida y middlewares estandarizados, te lo pone más fácil.

    Hono no te impide instrumentar trazas, pero requiere que diseñes la integración desde cero. Para infra ligera y métricas puntuales está bien; para auditoría y cumplimiento, NestJS acelera la adopción.

    Reglas prácticas para decidir (lista accionable)

    1. Despliegue Edge (Cloudflare Workers, Deno, Bun) → Hono. Docs: https://hono.dev, Cloudflare Workers: https://developers.cloudflare.com/workers
    2. Núcleo empresarial en Kubernetes → NestJS. Docs: https://docs.nestjs.com
    3. Proyecto nuevo pequeño, necesitas rendimiento en Node → Fastify sobre Express. Fastify: https://www.fastify.io
    4. Mantener app legacy en Express → parche, migración gradual a Fastify/NestJS o mantener si coste de migración es mayor.
    5. Necesitas tipado extremo end-to-end con frontend TS → Hono RPC o compartir DTOs desde NestJS.
    6. Requisito de cold starts <20ms → Hono (Edge); NestJS requiere contenedores siempre calientes.

    Patrón recomendado: combinar, no elegir a ciegas

    La arquitectura más práctica en 2026 no es monolítica en framework. Usa NestJS para la lógica de negocio, colas, trabajos en background y microservicios críticos; usa Hono para la capa perimetral, autenticación global, webhooks y endpoints que deben estar cerca del usuario.

    Ventaja: reduces coste y latencia donde importa, y mantienes previsibilidad y pruebas allí donde importa más: en el core.

    Conclusión

    Hono vs Express vs NestJS: cuál usar en 2026 no es una competición de popularidad. Es una cuestión de contexto. Si necesitas extremar latencia y desplegar en Edge, Hono gana. Si necesitas gobernanza de código, DI y un stack mantenible por equipos grandes, NestJS gana. Express sigue siendo válido solo para mantener legacy; para nuevo desarrollo, elige Fastify si trabajas exclusivamente en Node.

    Decide según ejecución y mantenimiento, no por hype. La mejor arquitectura combina herramientas: perímetro ligero (Hono) + corazón estructurado (NestJS). Eso te dará velocidad hoy y coherencia mañana.

    FAQ

    Respuesta: ¿Cuándo debo usar Hono en lugar de NestJS?

    Usa Hono cuando despliegues en runtimes Edge/Serverless (Cloudflare Workers, Deno, Bun) y necesites cold starts mínimos y alta concurrencia en endpoints públicos. Usa NestJS cuando necesites estructura, DI y gobernanza para equipos grandes.

    Respuesta: ¿Express todavía tiene sentido para proyectos nuevos?

    No para la mayoría de proyectos nuevos. Express se mantiene por legacy. Para nuevo desarrollo en Node, considera Fastify por rendimiento y plugins modernos.

    Respuesta: ¿Cómo afecta el entorno de ejecución a la elección del framework?

    El entorno define compatibilidad y cold starts. Hono está diseñado para la Fetch API y funciona en Workers/Deno/Bun sin polyfills. NestJS está pensado para contenedores y necesita procesos calientes para minimizar latencia.

    Respuesta: ¿Qué alternativas a Express recomiendan para Node moderno?

    Fastify es la alternativa recomendada por rendimiento y ecosistema de plugins. Evalúa Fastify sobre Express para proyectos nuevos en Node.

    Respuesta: ¿Puedo mezclar Hono y NestJS en la misma plataforma?

    Sí. Patrón recomendado: Hono en el perímetro (autenticación global, webhooks, endpoints edge) y NestJS en el núcleo (lógica de negocio, colas, trabajos). Separar responsabilidades reduce coste y mejora latencia.

    Respuesta: ¿Qué considerar sobre cold starts en serverless?

    Si tu SLA requiere latencias muy bajas y tienes picos geográficos, los cold starts importan. Hono puede arrancar en milisegundos en Edge; NestJS suele requerir contenedores calientes y arranques en cientos de ms.

  • Implementación de @supabase/server para Edge Functions Seguras

    Implementación de @supabase/server para Edge Functions Seguras

    npm install @supabase/server@latest npx skills add supabase/server — Cambia cómo piensas la seguridad y la infraestructura de tus Edge Functions

    Tiempo estimado de lectura: 4 min

    • Reduce boilerplate: @supabase/server entrega SupabaseContext por petición y primitives para evitar repetir validación de JWT, clientes anon/admin y CORS.
    • Seguridad declarativa: auth modes visibles por ruta ({ auth: ‘user’ }, ‘secret’, ‘none’, etc.) que simplifican auditorías.
    • Automatización para equipos: npx skills add supabase/server añade conocimiento a agentes para migraciones y scaffolds fiables.

    Hoy no es sobre instalar una dependencia. Es sobre cambiar cómo piensas la seguridad y la infraestructura de tus Edge Functions. Si ejecutas npm install @supabase/server@latest y luego npx skills add supabase/server, obtienes dos cosas: una librería que elimina el boilerplate y una “skill” para agentes de código que conoce la API, los patrones y las rutas de migración. Esto no es marketing; es ingeniería que reduce errores y acelera migraciones.

    Resumen rápido (lectores con prisa)

    Qué es: Un paquete que entrega SupabaseContext preconfigurado por petición y una skill para agentes.

    Cuándo usarlo: Al migrar o crear Edge Functions que requieran autenticación, roles o admin operations.

    Por qué importa: Reduce código repetido y errores; hace la seguridad declarativa.

    Cómo funciona: Wrapper middleware (withSupabase) o primitivas (createSupabaseContext) que validan auth y exponen clientes y claims.

    Qué hace la instalación (breve)

    npm install @supabase/server@latest

    Instala el paquete que crea un SupabaseContext preconfigurado por petición.

    npx skills add supabase/server

    Entrega ese contexto a agentes como Claude Code o Codex, para que puedan generar migraciones y scaffolds fiables.

    Fuentes y documentación

    Qué obtienes realmente: contextos y patrones repetibles

    Instalar @supabase/server no es solo añadir un helper: introduces un patrón consistente. El núcleo es el SupabaseContext que te dan los wrappers como withSupabase:

    • ctx.supabase: cliente con scope de usuario (respeta RLS)
    • ctx.supabaseAdmin: cliente con service role (operaciones privilegiadas)
    • userClaims / jwtClaims: identidad verificada
    • authMode: cómo se autenticó la petición

    Ejemplo mínimo (edge handler compatible con cualquier runtime)

    import { withSupabase } from 'npm:@supabase/server'
    
    export default {
      fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
        const { data } = await ctx.supabase.from('todos').select()
        return Response.json(data)
      }),
    }
    

    Eso es todo. Declaras auth y recibes contexto. Si la petición no tiene token válido, el middleware corta la ejecución antes de tocar tu lógica.

    Modos de autenticación: explícitos y visibles

    La seguridad deja de ser código disperso y pasa a ser una declaración:

    • { auth: 'user' } — solo usuarios con JWT válido
    • { auth: 'none' } — webhooks y health checks
    • { auth: 'secret' } — server-to-server con clave secreta
    • { auth: 'publishable' } — publishable key
    • { auth: ['user', 'secret'] } — acepta JWT o secret key

    Ventaja práctica: el modelo de seguridad de una función es legible en una línea. Auditar políticas, cumplir requisitos de cumplimiento o revisar riesgo queda mucho más simple.

    Migrar a JWT asimétricos sin dolor

    Antes de @supabase/server, migrar a las nuevas claves asimétricas significaba instalar jose, montar JWKS, exponer nuevos secretos y actualizar función por función. Con este paquete la validación asimétrica y la resolución de JWKS se gestionan internamente. Resultado: menos oportunidades de equivocarte y menos código repetido.

    Cuando necesitas control fino: createSupabaseContext y las primitivas

    No todo el mundo usa el wrapper. Si necesitas flujo imperativo o respuestas personalizadas, existe createSupabaseContext:

    import { createSupabaseContext } from 'npm:@supabase/server'
    
    export default {
      fetch: async (req) => {
        const { data: ctx, error } = await createSupabaseContext(req, { auth: 'user' })
        if (error) return Response.json({ error: error.message }, { status: error.status })
    
        const { data } = await ctx.supabase.from('todos').select()
        return Response.json(data)
      },
    }
    

    Y si tu arquitectura es aún más compleja (MCP servers, adaptadores personalizados) las primitivas core están disponibles: createAdminClient, createContextClient, resolveEnv, verifyAuth — las mismas funciones que alimentan los adaptadores oficiales.

    Hono y adaptadores: patrón simple para frameworks

    Hono fue el primer adaptador oficial. Un ejemplo con Hono muestra lo limpio que queda:

    import { withSupabase } from '@supabase/server/adapters/hono'
    import { Hono } from 'hono'
    
    const app = new Hono()
    app.get('/todos', withSupabase({ auth: 'user' }), async (c) => {
      const { supabase } = c.var.supabaseContext
      const { data } = await supabase.from('todos').select()
      return c.json(data)
    })
    export default { fetch: app.fetch }
    

    El adaptador inyecta supabaseContext en c.var y lista. Si necesitas otro framework, busca un adaptador o usa las primitivas core.

    Agentes y automatización: por qué añadir la “skill” es útil

    npx skills add supabase/server le da a un agente el conocimiento de la API y patterns. Eso permite prompts robustos:

    • “Analyze all Edge Functions and plan a migration to @supabase/server”
    • “Scaffold a Hono REST API with per-route auth for todos”
    • “Create an admin Edge Function that accepts user or secret auth and logs audits”

    Si tu equipo usa agentes para refactorizaciones o scaffolding, esta skill reduce la tasa de alucinaciones y acelera entregas.

    Criterio práctico para equipos técnicos

    • Empieza migrando endpoints críticos: aquellos con manipulación de roles o auditoría.
    • Sustituye shared utilities por withSupabase o createSupabaseContext: menos código repetido, menos errores.
    • Usa modes combinados (['user','secret']) para endpoints mixtos (frontend + cron jobs).
    • Revisa variables de entorno: SUPABASE_PUBLISHABLE_KEYS y SUPABASE_SECRET_KEYS en lugar de keys singulares.
    • Prueba con agentes para migraciones masivas, pero añade revisión humana.

    Conclusión

    npm install @supabase/server@latest y npx skills add supabase/server no son trucos de productividad; son una inversión en consistencia y seguridad. Si gestionas Edge Functions, adoptar este patrón reduce la superficie de error, acelera auditorías y libera tiempo para construir la lógica que realmente importa. Revisa la documentación en Documentación general y el repositorio en Repositorio Supabase para empezar.

    Dominicode Labs

    Para equipos que automatizan migraciones y workflows con agentes, una referencia práctica y recursos adicionales están disponibles en Dominicode Labs. Integrar estas prácticas con pipelines de automatización puede acelerar adopciones y reducir errores humanos.

    FAQ

    ¿Qué hace exactamente @supabase/server?

    Entrega un SupabaseContext preconfigurado por petición que incluye clientes (usuario y admin), claims y el modo de autenticación. También ofrece primitivas para escenarios imperativos o adaptadores de framework.

    ¿Necesito cambiar todas mis funciones para usarlo?

    No necesariamente. Puedes migrar endpoints críticos primero y sustituir utilidades compartidas por los wrappers o primitivias según prioridades del equipo.

    ¿Cómo maneja la validación de JWT asimétricos?

    La validación asimétrica y la resolución de JWKS se gestionan internamente por el paquete, evitando tener que instalar y montar jose y JWKS manualmente en cada función.

    ¿Qué diferencia hay entre withSupabase y createSupabaseContext?

    withSupabase es un wrapper middleware que inyecta contexto automáticamente y corta ejecución si la auth falla. createSupabaseContext es una primitiva imperativa útil cuando necesitas control fino sobre la respuesta o flujo.

    ¿La skill para agentes reemplaza la revisión humana?

    No. La skill reduce la tasa de alucinaciones y acelera la generación de migraciones y scaffolds, pero se recomienda revisión humana antes de despliegues críticos.

    ¿Dónde encuentro más documentación y ejemplos?

    Consulta la Documentación general y el Repositorio Supabase para ejemplos, adaptadores y guías de migración.

  • Cómo construir un agente de IA con NestJS y Claude API

    Cómo construir un agente de IA con NestJS y Claude API

    Cómo construir un agente de IA con NestJS + Claude API

    Tiempo estimado de lectura: 4 min

    • Separar cliente LLM, registro de herramientas y loop de agente para modularidad y pruebas.
    • Herramientas como contratos JSON-schema y validación antes de ejecución.
    • Agent loop controlado: límites de iteraciones, métricas y manejo de errores normalizado.
    • Producción: no bloquear peticiones HTTP, usar SSE/WebSockets y proteger PII y costes.
    • Observabilidad y testing: trazas por sesión, mocks para provider y canary releases.

    Introducción

    Saber cómo construir un agente de IA con NestJS + Claude API es lo que separa una demo interesante de una pieza de infraestructura que puedas mantener en producción. En este artículo encontrarás la arquitectura, decisiones técnicas y patrones que realmente importan cuando implementas tool-calling de Claude dentro de un backend modular y tipado como NestJS.

    Un agente no es un chatbot: es un bucle de decisión. Recibe objetivo, decide herramientas, ejecuta, observa resultados y vuelve a razonar hasta resolver la tarea o agotar límites.

    Resumen rápido (lectores con prisa)

    Qué: Un agente es un bucle de decisión que orquesta llamadas a herramientas externas desde un LLM.

    Cuándo: Cuando necesitas que un LLM interactúe con sistemas externos (DB, APIs, logs) de forma controlada.

    Por qué importa: Permite trazabilidad, testing y control de costes frente a soluciones ad-hoc.

    Cómo: Separando un provider LLM, un registro de herramientas con schemas JSON y un agente que controla el loop y límites.

    Cómo construir un agente de IA con NestJS + Claude API: arquitectura y flujo

    Ese bucle —Agent Loop— obliga a diseñar responsabilidades claras. El agente recibe un objetivo, decide qué herramienta usar, ejecuta esa herramienta, observa el resultado y repite hasta resolver la tarea o agotar límites.

    Arquitectura mínima recomendada:

    • Proveedor del cliente LLM (Anthropic) como Provider de NestJS.
    • Registro dinámico de herramientas (ToolRegistryService) que mapea nombres/JSON-schema a métodos de servicios.
    • Motor de ejecución (AgentService) que orquesta el loop y gestiona historial, stop reasons y seguridad.

    Referencias: NestJS Docs y Anthropic Tool Use

    Capa 1 — AnthropicProvider: el cliente como dependencia inyectable

    Nunca newees el cliente de Anthropic en controladores o servicios. Crea un provider que se inyecte y centralice la lógica del cliente.

    • Lee ANTHROPIC_API_KEY mediante ConfigService.
    • Envuelve lógica de retries, backoff y métricas (tokens usados, latencia).
    • Permite mockear en tests unitarios.

    Ejemplo conceptual: el provider expone sendMessage(payload) que encapsula anthropic.messages.create() y normaliza la respuesta (stop_reason, content, tool_call).

    Beneficio: centralizas control de coste y modelo (p. ej. cambiar de Claude 3.5 a otro modelo sin tocar el resto).

    Capa 2 — ToolRegistry: herramientas como contratos JSON y servicios

    Claude espera herramientas descritas por JSON-schema. En el backend conviene modelarlas y validar antes de ejecutar.

    • Definir cada herramienta con nombre, descripción y schema (tipado TypeScript).
    • Mapear cada herramienta a un método de servicio inyectable (p. ej. UsersService.getById, LogsService.append).
    • Implementar granularidad: una herramienta = una responsabilidad.

    Diseño práctico:

    • ToolRegistryService mantiene un mapa { toolName -> { schema, executor } }.
    • executor(args) valida los args contra el schema y ejecuta el método correspondiente en try/catch.

    Así el agent loop no necesita switches gigantes; resuelve métodos dinámicamente.

    Capa 3 — AgentService: el bucle, stop_reasons y límites

    El AgentService orquesta el loop de decisión, mantiene el historial y aplica límites y políticas de seguridad.

    Patrón del Agent Loop

    1. Construir messages + tools (esquemas) y llamar a Anthropic.
    2. Leer stop_reason:
      • end_turn: devolver respuesta final.
      • tool_use: extraer tool_name y arguments.
    3. Ejecutar herramienta vía ToolRegistryService y añadir tool_result al historial.
    4. Repetir hasta end_turn o alcanzar un límite de iteraciones.

    Normas prácticas

    • Límite de iteraciones (p. ej. max 5) para evitar bucles y costes excesivos.
    • Cada iteración registra métricas: tokens, latencia, herramienta invocada.
    • Normaliza errores: si la herramienta falla, devuelve { error: 'timeout' } a Claude, no throws.

    Producción: latencia, UX y seguridad

    Al pasar a producción hay que priorizar experiencia de usuario, seguridad y observabilidad.

    Latencia y UX

    • No bloquees la petición HTTP principal. Emite progreso con SSE o WebSockets: “Pensando…”, “Consultando DB…”.
    • Opcional: respuesta rápida + notificación cuando el resultado final esté listo (webhook / push).

    Seguridad y validación

    • Valida argumentos de herramientas con class-validator/schema JSON antes de ejecutar.
    • Logs sensibles: evita enviar secrets o PII a la API de Claude.
    • Rate limits y circuit breakers en el provider para proteger tu backend y controlar facturación.

    Observabilidad

    Traza cada sesión como un árbol de spans: requests al LLM, ejecuciones de herramientas, errores.

    Integra herramientas de visualización y trazabilidad como Langfuse o LangSmith para visualizar flujos y coste por sesión.

    Testing y despliegue

    • Mockea el AnthropicProvider y el ToolRegistryService en tests unitarios.
    • Tests de integración: entorno con Claude sandbox o replay de respuestas deterministas.
    • Canary releases: habilita el agente en subset de usuarios antes de producir a toda la base.

    Coste y gobernanza

    • Mide tokens por iteración; extrapola a coste por sesión.
    • Define políticas: cuándo permitir tool-calling (p. ej. solo usuarios verificados) y límites diarios.

    Qué evitar (errores comunes)

    • Herramientas “dios” que hacen todo: dificultan autorización y testing.
    • Dejar excepciones sin capturar: provoca loops rotos y mala UX.
    • No auditar llamadas: sin trazabilidad no sabrás por qué el agente falló o costó tanto.

    Cierre práctico

    Construir un agente con NestJS + Claude API no es magia, es disciplina arquitectónica. Si abstraes el cliente, modelas herramientas como contratos y controlas el bucle con límites, obtienes un sistema escalable, testeable y seguro.

    En el siguiente artículo veremos ejemplos concretos de ToolRegistryService y patrones para emitir progreso en SSE desde NestJS para mejorar la experiencia del usuario.

    Dominicode Labs

    Para continuidad en temas de automatización y agentes, consulta recursos adicionales en Dominicode Labs. Es una fuente útil para patrones, ejemplos y plantillas prácticas que complementan esta arquitectura.

    FAQ

    ¿Qué es un agente de IA en este contexto?

    Un agente es un bucle de decisión que recibe un objetivo, decide qué herramienta invocar, ejecuta esa herramienta, observa el resultado y repite hasta completar la tarea o agotar límites.

    ¿Por qué usar un provider para Anthropic?

    Un provider centraliza la configuración del cliente (API key, retries, métricas), facilita el mock en tests y permite cambiar de modelo o proveedor sin modificar la lógica de negocio.

    ¿Cómo se describen las herramientas?

    Las herramientas se describen con nombre, descripción y un JSON-schema (tipado TypeScript). Se valida la entrada antes de ejecutar y se mapea a funciones/executors inyectables.

    ¿Qué es stop_reason y cómo se maneja?

    stop_reason indica la acción del LLM: end_turn para respuesta final o tool_use para invocar una herramienta. El agente interpreta y actúa según ese valor.

    ¿Cómo se asegura la producción respecto a latencia?

    No bloquear la petición HTTP principal; usar SSE o WebSockets para emitir progreso. También considerar respuestas rápidas con notificación posterior y aplicar límites de iteraciones para controlar latencia y coste.

    ¿Qué pruebas son recomendables?

    Mockear el provider y el registry en tests unitarios; usar sandbox o replays deterministas en integración; ejecutar canary releases antes de un despliegue completo.

  • Mejorando rendimiento y SEO al migrar de Angular a Next.js 16

    Mejorando rendimiento y SEO al migrar de Angular a Next.js 16

    De Angular a Next.js 16: lo que aprendí migrando un proyecto real

    Tiempo estimado de lectura: 4 min

    • Rendimiento y SEO fueron el motor: migramos por Core Web Vitals malos, TTFB lento y bundle que penalizaba conversión móvil.
    • Server-first cambia la mentalidad: Next.js 16 y React Server Components mueven carga y lógica al servidor, reduciendo JavaScript en cliente.
    • Menos boilerplate para mutaciones: Server Actions permiten llamar funciones server-side desde formularios sin endpoints REST intermedios.
    • Fricciones reales: cache, alcance de “use client” y observabilidad requieren disciplina adicional en producción.

    De Angular a Next.js 16: lo que aprendí migrando un proyecto real empezó como un problema de negocio: Core Web Vitals malos, TTFB lento y un bundle que penalizaba conversión móvil. La migración no fue una moda técnica; fue una necesidad para reducir fricción de usuario y mejorar SEO técnico. Esto marcó cada decisión técnica que tomamos.

    Resumen rápido (lectores con prisa)

    Qué es: Next.js 16 (App Router) usa React Server Components para renderizar HTML en servidor y enviar JavaScript mínimo al cliente.

    Cuándo usarlo: cuando SEO, Core Web Vitals o TTFB afectan métricas de negocio y necesitas ejecutar lógica sensible en servidor.

    Por qué importa: reduce bundle inicial, mejora TTFB y simplifica flujos de datos server-side.

    Cómo funciona (resumen): renderizado server-side con funciones asíncronas para fetch/ORM, Server Actions para llamadas server desde forms y control explícito de caché y revalidación.

    De Angular a Next.js 16: por qué no es solo “aprender otra sintaxis”

    Angular es un framework opinado para SPAs: inyección de dependencias, RxJS y templates declarativos. Next.js 16 (App Router) invierte ese paradigma con React Server Components (RSC): renderizado en servidor, HTML entregado al cliente y JavaScript mínimo para interactividad. Documentación oficial Next.js.

    La diferencia no es menor: pasas de pensar “qué corre en el cliente” a “qué debe correr en el servidor”. Ese cambio impacta performance, seguridad y la forma en que estructuras estado y dependencias.

    Tres lecciones técnicas que cambiaron nuestro código

    1) RxJS se queda fuera del camino principal

    En Angular, RxJS orquesta peticiones, eventos y sincronizaciones. Eso ofrece control fino (cancelaciones, operadores), pero añade complejidad de mantenimiento (unsubscribe, memory leaks).

    En Next.js 16, Server Components son funciones async: await fetch() o llamadas al ORM desde el servidor. La simplicidad reduce boilerplate y evita parpadeos de carga en el cliente. Ejemplo real: reemplazar múltiples subscriptions por una única llamada asíncrona en el server simplificó la lógica y redujo errores de sincronización.

    Nota práctica: para cancelaciones del lado del cliente hay que usar explícitamente AbortController; la ergonomía de RxJS no existe por defecto.

    2) La inyección de dependencias se reimagina

    El contenedor DI de Angular es una comodidad arquitectónica (services providedIn: 'root'). React/Next no tienen un DI integrado. Las alternativas que adoptamos:

    • Instancias únicas exportadas desde módulos ES6 (clientes DB, SDKs).
    • React Context solo para estado UI que vive en cliente (tema, sesión).
    • Props/Composición para inyección explícita en componentes que dependen de servicios.

    Resultado: más explicitud y trazabilidad, pero más disciplina para no propagar dependencias globales por accidente.

    3) Server Actions: menos endpoints, menos boilerplate

    Migrar formularios del flujo Angular (form → HttpClient → endpoint REST → backend) a Server Actions colapsó la cadena. En Next.js 16 puedes llamar funciones en el servidor directamente desde el form:

    export async function updateUser(formData: FormData) {
      'use server';
      const name = formData.get('name') as string;
      await db.user.update({ where: { id: session.userId }, data: { name } });
      revalidatePath('/profile');
    }

    El beneficio es claro: menos endpoints internos y menos código repetitivo. El riesgo: mezclar lógica de negocio en componentes si no separamos responsabilidades adecuadamente. Docs de Server Actions

    Fricciones reales que te van a doler en producción

    • Cache y freshness: Next.js App Router tiene capas de caché (memoization, data cache, route cache). Sin revalidate o cache: 'no-store' puedes servir datos obsoletos. Leer.
    • “use client” propagate cost: marcar un componente como cliente arrastra su subárbol y puede romper los beneficios del SSR si importas librerías pesadas.
    • Observabilidad de comportamiento: la frontera servidor/cliente exige testing más exhaustivo (end-to-end + integración server actions) y pipelines de CI que validen rendimiento.
    • Seguridad y surface area: Server Actions facilitan lógica server-side, pero exigen revisar permisos y sanitización con más rigor.

    Criterio práctico para Tech Leads: ¿vale la pena migrar?

    No migres por moda. Migra si:

    • Tu producto es público y SEO o Core Web Vitals impactan conversiones (ver métricas en web.dev/vitals).
    • El bundle inicial y TTFB están bloqueando métricas de negocio.
    • Necesitas ejecutar lógica sensible en servidor para reducir exposición o proteger IP.

    Mantén Angular si:

    • Es un dashboard interno con poca necesidad SEO.
    • El equipo domina RxJS y la arquitectura actual es sostenible.
    • El coste de migración supera el beneficio económico esperado.

    Conclusión

    La migración De Angular a Next.js 16: lo que aprendí migrando un proyecto real fue menos una reescritura técnica y más una reorganización de responsabilidades: qué corre en el servidor, cómo se inyectan dependencias y cómo se gestionan mutaciones. Next.js 16 ofrece ganancias reales en rendimiento y simplicidad operativa, pero exige disciplina (caché, límites use client, separación de responsabilidades). Si tu negocio lo justifica, la inversión devuelve rendimiento y una arquitectura más alineada con un futuro server-first. Si no, Angular sigue siendo una opción sólida y productiva.

    FAQ

    ¿Por qué migraron desde Angular?

    Migramos porque Core Web Vitals malos, TTFB lento y un bundle grande estaban afectando conversión móvil y SEO. La migración fue una decisión de negocio para reducir fricción de usuario y mejorar SEO técnico.

    ¿Qué reemplaza a RxJS en Next.js 16?

    No hay un reemplazo directo. En Next.js 16 se usa programación asíncrona en Server Components (await fetch(), llamadas al ORM) y, para cancelaciones cliente, AbortController. La lógica de orquestación que RxJS ofrecía suele simplificarse en el servidor o con patrones de composición en cliente.

    ¿Cómo funcionan las Server Actions?

    Server Actions son funciones que se ejecutan en el servidor y se pueden invocar desde formularios en el cliente. Reducen la necesidad de endpoints REST intermedios. Requieren separar responsabilidades para no mezclar lógica de negocio en componentes. Más detalles.

    ¿Cuáles son los mayores riesgos en producción?

    Los riesgos principales son caché y freshness (servir datos obsoletos sin revalidate), el coste de marcar componentes como cliente que arrastran subárboles pesados y la necesidad de mayor observabilidad y testing para la frontera servidor/cliente.

    ¿Cuándo no conviene migrar?

    No conviene migrar si el proyecto es un dashboard interno con poca necesidad SEO, si el equipo domina la arquitectura actual o si el coste de migración supera el beneficio económico esperado.

    ¿Cómo gestionar la inyección de dependencias sin DI?

    Usar instancias únicas exportadas desde módulos ES6 (clientes DB, SDKs), React Context para estado UI cliente y props/composición para inyección explícita en componentes. Esto aporta trazabilidad a costa de disciplina para evitar dependencias globales indeseadas.

  • Diferencias clave entre XMLHttpRequest y fetch() en JavaScript

    Diferencias clave entre XMLHttpRequest y fetch() en JavaScript

    ¿Cuáles son las diferencias entre XMLHttpRequest y fetch() en JavaScript y en los navegadores?

    Tiempo estimado de lectura: 3 min

    • XHR es una API veterana orientada a eventos y callbacks; fetch() es la API moderna basada en Promesas, diseñada para integrarse con Service Workers y Streams.
    • fetch() no rechaza por códigos HTTP; hay que comprobar response.ok. XHR requiere revisar status en onload.
    • Cancelación y progreso difieren: XHR tiene .abort() y upload.onprogress; fetch() usa AbortController y Streams para descarga.
    • Compatibilidad: XHR es universal (incluido IE); fetch() es moderno y puede requerir polyfill para entornos legacy.

    Introducción

    Si trabajas con la red en el navegador, tarde o temprano te encontrarás con esta pregunta: ¿Cuáles son las diferencias entre XMLHttpRequest y fetch() en JavaScript y en los navegadores? La respuesta no es solo sintaxis: es arquitectura, garantías y compromisos. Aquí tienes lo que realmente importa —con ejemplos y criterio técnico— para decidir con fundamento.

    En una línea: XHR es una API veterana basada en eventos y callbacks; fetch() es la API moderna basada en Promesas, diseñada para integrarse con Service Workers, Streams y async/await. Pero esa frase no resuelve bugs. Vamos por partes.

    Resumen rápido (lectores con prisa)

    Qué es: XHR es una API basada en eventos; fetch() es una API basada en Promesas y diseñada para la web moderna.

    Cuándo usarlo: Usa fetch() por defecto en proyectos modernos; conserva XHR si necesitas upload progress o soporte legacy sin polyfills.

    Por qué importa: Comportamientos sobre errores, cancelación y progreso difieren y pueden introducir bugs sutiles.

    Cómo funciona (breve): XHR expone estados y callbacks; fetch() devuelve Promesas y se integra con AbortController y Streams.

    Paradigma y legibilidad

    XMLHttpRequest (XHR): modelo orientado a eventos. Listeners, estados (readyState), comprobaciones manuales del status. Código más verboso y propenso a anidaciones.

    fetch(): devuelve una Promesa. Compatible con async/await. Composición y manejo más claro de flujos asíncronos.

    Ejemplo mínimo

    // XHR
    const xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/data');
    xhr.onload = () => { if (xhr.status===200) console.log(xhr.responseText) };
    xhr.send();
    
    // fetch
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error(res.status);
    console.log(await res.text());
    

    Manejo de errores HTTP (el detalle que rompe apps)

    fetch() no rechaza la promesa por códigos HTTP 4xx/5xx. Solo rechaza por fallos de red. Debes comprobar response.ok o response.status. XHR tampoco “lanza” un error automático: ahí el patrón habitual siempre ha sido revisar xhr.status en onload. La confusión surge al emigrar sin ajustar esa validación (documentado en MDN: Using Fetch).

    Cancelación de peticiones

    XHR: tiene .abort() en la propia instancia.

    fetch(): usa AbortController. Más explícito y reutilizable, pero añade un pequeño boilerplate.

    const controller = new AbortController();
    fetch('/api', { signal: controller.signal });
    controller.abort(); // cancela
    

    Progreso de subida (upload progress)

    Aquí XHR mantiene ventaja práctica: xhr.upload.onprogress ofrece bytes transferidos y total, ideal para barras de progreso en subidas grandes. fetch() puede manejar progreso de descarga con ReadableStream (Streams API), pero el progreso de subida no está estandarizado de forma simple en todos los navegadores. Si tu app hace uploads pesados, XHR o una librería que lo soporte sigue siendo la opción más directa.

    Streams y Service Workers

    fetch() está pensado para la web moderna: integración nativa con Service Workers y la Streams API, lo que permite estrategias offline y control fino de respuesta incrementales. XHR no tiene esa integración (spec Fetch: WHATWG spec).

    Cookies y credenciales (CORS)

    XHR: withCredentials = true para enviar cookies en peticiones cross-origin.

    fetch(): usa credentials (p. ej. credentials: 'include'). El comportamiento por defecto ha cambiado con el tiempo; sé explícito para evitar sorpresas.

    Compatibilidad y polyfills

    XHR es compatible con todos los navegadores (incluido IE). fetch() está disponible en navegadores modernos; para soporte legacy hay que polyfillear o usar librerías (ver MDN: XMLHttpRequest).

    Tabla rápida (resumen técnico)

    Característica XMLHttpRequest fetch()
    Paradigma Eventos/callbacks Promesas / async-await
    Errores HTTP Revisar status en onload Requiere response.ok
    Cancelación .abort() AbortController
    Upload progress Sí (upload.onprogress) No estandarizado
    Streams / Service Workers No Sí
    Compatibilidad Universal (legacy) Modern browsers (polyfill posible)

    Criterio práctico: ¿cuál elegir?

    Elige fetch() por defecto en proyectos modernos. Mejor integración con async/await, Service Workers y arquitectura actual.

    Conserva XHR (o usa una librería que lo abstraiga, como Axios) si necesitas:

    • Progreso de subida preciso.
    • Soporte sin polyfills para navegadores antiguos.

    Usa abstracciones del ecosistema (Angular HttpClient, Axios) cuando necesites interceptores, retry policies o consistencia entre entornos; pero conoce las capas inferiores para depurar.

    Recursos y lecturas

    Conclusión

    No es una cuestión de “moderno contra antiguo” sino de garantías. Saber qué comportamientos esperan ambas APIs (especialmente sobre errores, cancelación y progreso) te evita bugs sutiles en producción. Elijas lo que elijas, hazlo con conocimiento: ese es el criterio que diferencia código que sobrevive a equipos y tiempo del que solo sobrevive a una urgencia.

    FAQ

    Respuesta: ¿fetch() rechaza la promesa en errores HTTP (4xx/5xx)?

    No. fetch() solo rechaza la promesa por fallos de red u errores de infraestructura. Para errores HTTP debes comprobar response.ok o response.status y manejar el flujo correspondiente.

    Respuesta: ¿Cómo cancelo una petición fetch?

    Usa un AbortController y pasa su señal en las opciones: fetch(url, { signal: controller.signal }). Llama a controller.abort() para cancelar.

    Respuesta: ¿Puedo obtener progreso de subida con fetch()?

    No de forma estandarizada en todos los navegadores. Para progreso de subida preciso sigue usando XHR (xhr.upload.onprogress) o una librería que lo soporte.

    Respuesta: ¿Necesito polyfill para fetch() en producción?

    Depende de tu público objetivo. Si necesitas soportar navegadores legacy (por ejemplo IE) tendrás que polyfillear fetch() o usar alternativas como XHR o Axios.

    Respuesta: ¿Cuál es la ventaja de fetch() con Service Workers?

    fetch() se integra nativamente con Service Workers y la Streams API, permitiendo estrategias offline, cacheo avanzado y control incremental de respuestas.

    Respuesta: ¿Qué debo usar para compatibilidad con IE?

    Usa XMLHttpRequest directamente o una librería/polyfill para fetch(). XHR es compatible sin polyfills en entornos legacy.

  • Cómo usar Map.getOrInsert() en JavaScript para mejorar tu código

    Cómo usar Map.getOrInsert() en JavaScript para mejorar tu código

    Map.getOrInsert(): el método que siempre quisiste en JavaScript

    Tiempo estimado de lectura: 4 min

    • Menos código, más intención: getOrInsert/getOrInsertComputed expresan “devuelve o crea” en una llamada atómica.
    • Mejor tipado en TypeScript 6.0: el retorno es T, no T | undefined, reduciendo aserciones peligrosas.
    • Uso práctico: útil en cachés, agrupaciones, contadores y construcción de grafos; evita inicializaciones innecesarias.

    Resumen rápido (lectores con prisa)

    Map.getOrInsert y Map.getOrInsertComputed permiten obtener un valor existente o insertar uno nuevo en una sola operación. En TypeScript 6.0, con target: "esnext" y lib: ["esnext"], estos métodos están tipados para devolver T en vez de T | undefined. Use getOrInsert cuando quiera expresar “devuelve o crea” de forma atómica; use la variante computada para evitar inicializaciones costosas salvo que sean necesarias.

    Map.getOrInsert(): el método que siempre quisiste en JavaScript (qué es y cómo funciona)

    Map.getOrInsert(): el método que siempre quisiste en JavaScript aparece en la primera línea porque no es una mejora menor: es la forma de expresar, de manera atómica y tipada, la intención que antes requería tres operaciones verbosas. TypeScript 6.0 ya expone los tipos vía esnext y la propuesta ECMAScript que la introduce ha alcanzado Stage 4. Aquí te explico qué hace, por qué importa y cómo integrarlo con criterio en código de producción.

    Qué hace y cómo funciona

    El patrón clásico —buscar, insertar si no existe, y luego usar— se repite en cachés, agrupaciones, contadores y grafos. Tradicionalmente escribíamos:

    if (!map.has(key)) map.set(key, compute());
    const v = map.get(key)!;

    Tres operaciones, doble lookup y una aserción no nula que silencia al compilador. getOrInsert y su variante perezosa getOrInsertComputed resuelven eso en una sola llamada:

    • map.getOrInsert(key, defaultValue): devuelve el valor si existe, si no inserta y devuelve el defaultValue.
    • map.getOrInsertComputed(key, () => value): ejecuta la función sólo si la clave no existía, evitando inicializaciones innecesarias y efectos secundarios.

    En TypeScript 6.0, usando target: "esnext" y lib: ["esnext"], el compilador tipa estos métodos de forma que el retorno es T (no T | undefined). El contrato refleja la intención: si llamas, obtienes un valor seguro.

    Por qué esto no es solo “azúcar sintáctico”

    Tres razones prácticas:

    1. Menos errores de tipo

    Con el patrón manual el compilador ve get() como T | undefined. Con getOrInsert obtienes T. Menos comprobaciones defensivas, menos ! peligrosos.

    2. Menos overhead y operaciones atomizadas

    Evitas doble lookup (has + get). Aunque la mejora de rendimiento es pequeña en muchos casos, en bucles grandes o inicializaciones masivas suma.

    3. Menos ruido cognitivo

    La intención queda expresada: “devuelve o crea”. El lector del código no necesita reconstruir el propósito tras tres líneas.

    Casos reales donde cambia la vida

    – Cachés con inicialización costosa: evita ejecutar la función de cálculo salvo que sea estrictamente necesario.

    – Agrupaciones: Map<string, T[]> ya no requiere crear arrays temporales.

    – Contadores y histogramas: inicializar un contador a 0 en una sola línea es más legible.

    – Construcción de grafos (listas de adyacencia): evita boilerplate en algoritmos BFS/DFS.

    Ejemplo típico

    const groups = new Map<string, User[]>();
    
    function addUser(role: string, user: User) {
      groups.getOrInsertComputed(role, () => []).push(user);
    }

    Sencillo, explícito y sin crear arrays inútiles.

    Integración práctica en repositorios TypeScript

    1. Habilita esnext en tsconfig:
      {
        "compilerOptions": {
          "target": "esnext",
          "lib": ["esnext"],
          "strict": true
        }
      }
    2. Revisa compatibilidad runtime:

      TypeScript te da la comprobación estática. La API como tal puede requerir polyfill o verificar la versión de Node/Bun/deno si no está implementada nativamente en tu motor.

    3. Refactor incremental:

      Busca utilidades internas con nombres como getOrCreate, getOrInit o computeIfAbsent. Reemplaza utilidades por el método nativo en PRs pequeños. Añade pruebas unitarias para casos bordes (excepciones en la función de cálculo, concurrencia en entornos compartidos).

    4. No abuses de getOrInsertComputed con side effects:

      La función de cálculo se invoca solo cuando hace falta, pero debe ser determinista y preferiblemente sin efectos secundarios que el mapa pueda registrar de forma incompleta si ocurre una excepción.

    Consideraciones de diseño y criterio Dominicode

    Estándar sobre conveniencia: si ya tienes utilidades internas, migrar a getOrInsert reduce mantenimiento y mejora la expresividad.

    No sustituyas estructuras lógicas: usar Map con getOrInsert no es excusa para diseños pobres. Sigue definiendo responsabilidades y límites de módulo.

    Audita y documenta: añade esta patrón a las guías internas y revisiones de código para homogenizar su uso.

    Fuentes y lectura adicional

    Adoptar Map.getOrInsert() hoy es reducir ruido, reforzar tipos y expresar intención. No es una moda: es una higiene mínima que facilita mantener bases de código grandes sin perder claridad. Si eres Tech Lead, estandariza su uso; si eres desarrollador, empieza a buscar esos utils/mapGetOrCreate.ts y reemplázalos por la API del lenguaje.

    FAQ

    ¿Qué devuelve getOrInsert si la clave existe?

    Respuesta — ¿Qué devuelve getOrInsert si la clave existe?

    Devuelve el valor existente asociado a la clave. El contrato asegura que el retorno es T, no T | undefined.

    ¿En qué se diferencia getOrInsert de getOrInsertComputed?

    Respuesta — ¿En qué se diferencia getOrInsert de getOrInsertComputed?

    getOrInsert recibe un valor por defecto ya construido; getOrInsertComputed recibe una función que se ejecuta solo si la clave no existe, evitando costes de inicialización cuando no son necesarios.

    ¿Necesito un polyfill para usarlo en producción?

    Respuesta — ¿Necesito un polyfill para usarlo en producción?

    Depende del runtime. TypeScript ofrece comprobación estática cuando apuntas a esnext, pero la API puede no estar presente en versiones antiguas de Node/Bun/deno; en esos casos se requiere polyfill o verificación de la versión del motor.

    ¿Cómo mejora el tipado en TypeScript 6.0?

    Respuesta — ¿Cómo mejora el tipado en TypeScript 6.0?

    Con target: "esnext" y lib: ["esnext"], los tipos para estos métodos hacen que el retorno sea T, evitando la necesidad de aserciones (!) y reduciendo comprobaciones manuales de undefined.

    ¿Es seguro usarlo en entornos concurrentes?

    Respuesta — ¿Es seguro usarlo en entornos concurrentes?

    El método evita doble lookup, pero la seguridad en entornos concurrentes depende del runtime y del modelo de concurrencia. Añade pruebas y, si corresponde, mecanismos de sincronización en entornos compartidos.

    ¿Debo reemplazar todas mis utilidades internas?

    Respuesta — ¿Debo reemplazar todas mis utilidades internas?

    No necesariamente. Migra en PRs pequeños y cuando aporte claridad o reduzca mantenimiento. Prioriza casos donde el beneficio sea evidente (cachés, agrupaciones, contadores).

    ¿Qué precauciones al usar funciones con efectos secundarios?

    Respuesta — ¿Qué precauciones al usar funciones con efectos secundarios?

    La función de cálculo en getOrInsertComputed debe ser determinista y preferiblemente sin efectos secundarios. Si lanza una excepción, el mapa podría quedar en un estado intermedio; maneja errores y añade pruebas para esos casos.

  • Migración a Signal Forms en Angular 22: Mejorando formularios reactivos

    Migración a Signal Forms en Angular 22: Mejorando formularios reactivos

    Signal Forms estable: el nuevo estándar de formularios en Angular 22

    Tiempo estimado de lectura: 4 min

    • Signal Forms reemplaza Observables por Signals nativos para exponer valor, validez y estados de control.
    • Mejora ergonomía y rendimiento al evitar suscripciones manuales y emitir reactividad síncrona y dirigida.
    • Encaja con una arquitectura Zoneless para re-rendering quirúrgico y menor sobrecarga en formularios complejos.
    • Migración pragmática: usar toSignal() y desacoplar validadores reduce el coste de adopción.

    Introducción

    Signal Forms estable aparece como la evolución natural que consolida el manejo de formularios basado en Signals en Angular 22. Signal Forms estable reemplaza la dependencia de RxJS en la capa de interfaz por un modelo de reactividad síncrono y explícito, alineado con la arquitectura Zoneless y Signals centrales del framework.

    La propuesta no es solo sintaxis; es un cambio de ergonomía y rendimiento. Aquí explico qué cambia, por qué importa y cómo preparar una migración pragmática en proyectos reales.

    Resumen rápido (lectores con prisa)

    Signal Forms expone estado de formularios como Signals nativos en lugar de Observables. Use Signals para leer estado y computed() para derivar valores síncronos. Migración pragmática: convertir valueChanges a Signals con toSignal() y desacoplar validadores.

    ¿Qué es Signal Forms estable y qué problema resuelve?

    Signal Forms estable expone el estado del formulario —valor, validez, touched/dirty— como Signals nativos en lugar de streams Observables. Los problemas que resuelve de forma directa:

    • Evita gestión manual de suscripciones (memory leaks).
    • Elimina desfases causados por emisiones asíncronas en validaciones cruzadas.
    • Encaja de forma nativa con una arquitectura Zoneless, donde Signals notifican de forma quirúrgica qué partes del DOM actualizar.

    Fuente y discusión activa sobre el diseño: discusiones en GitHub. Para contexto sobre Signals y reactividad en Angular: guía de Signals en Angular.

    Cambio conceptual: de observar eventos a leer estado derivado

    Con ReactiveFormsModule hoy suelen usarse propiedades como valueChanges o statusChanges (Observables). Signal Forms cambia el patrón: en lugar de suscribirte, lees un Signal o creas computed() que derive estados complejos.

    Comparativa rápida:

    // ReactiveForms (actual)
    readonly isValid$ = this.form.statusChanges.pipe(
      map(s => s === 'VALID'),
      distinctUntilChanged()
    );
    
    // Signal Forms (conceptual)
    readonly isValid = computed(() => this.form.status() === 'VALID');
    

    computed() es síncrono, no requiere teardown manual y se integra con el grafo de dependencias para reevaluar solo cuando los valores relevantes cambian.

    Validaciones cruzadas y tracking de dependencias

    Las validaciones cruzadas son donde RxJS más fricción introduce: operadores para evitar bucles, distinct checks, y micro-delays. Con Signals, el runtime realiza dependency tracking: si una validación depende de A y B, se reevaluará solo cuando A o B cambien, sin operadores adicionales ni emisiones redundantes.

    Esto reduce tanto complejidad como carga en el hilo principal en formularios con muchos campos interrelacionados (CRMs, ERPs).

    Integración con Zoneless y pipeline de rendimiento

    La llegada de Signal Forms completa el modelo Zoneless (ver: guía Zoneless). En una app Zoneless:

    • Cada actualización de control escribe en un Signal.
    • Angular identifica qué templates dependen de ese Signal.
    • Solo esos nodos se re-renderizan.

    Resultado: menos trabajo innecesario en eventos de alta frecuencia y trazas de depuración limpias (sin contaminación por Zone.js). Para contexto sobre migraciones Zoneless y Signals, consulta: guía de Signals en Angular.

    Ejemplo práctico de interoperabilidad temporal

    Antes de la estabilización completa, la forma pragmática de probar el patrón es convertir valueChanges a Signal con toSignal (rxjs-interop).

    import { toSignal } from '@angular/core/rxjs-interop';
    
    this.formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
    
    computed(() => {
      const value = this.formValue();
      // derivaciones y validaciones sincronas aquí
    });
    

    Esto ofrece un puente entre ReactiveForms y lo que Signal Forms hará nativo.

    Guía de rxjs-interop: guía de rxjs-interop.

    Estado de la API y compatibilidad futura

    La API está en RFC y discusión, y lo más probable es que conviva con ReactiveFormsModule durante varias versiones para minimizar rupturas. El equipo de Angular apunta a compatibilidad con validadores existentes y a proporcionar herramientas de migración (schematics) en el momento del lanzamiento estable.

    Sigue las discusiones oficiales: discusiones en GitHub.

    Estrategia práctica para Tech Leads y equipos

    No reescribas todo hoy. Sí aplica estas medidas para reducir el coste de migración:

    • Desacopla la lógica de validación y transformación del FormGroup. Mantén funciones puras o servicios para reglas de negocio.
    • Introduce toSignal() donde tenga sentido para que los templates y la lógica consuman estado de forma síncrona.
    • Establece ChangeDetectionStrategy.OnPush en componentes nuevos para asegurar un modelo de render predecible.
    • Automatiza pruebas E2E que cubran flujos de validación cruzada y efectos secundarios.
    • Reserva una fase de migración por componentes: empezar por formularios sencillos y luego los complejos.

    Checklist mínimo antes de activar Signal Forms en producción

    • Lógica desacoplada (validadores fuera del FormGroup).
    • Cobertura de pruebas para validación y envíos.
    • Observabilidad en producción para comparar métricas (TTI, LCP).

    Conclusión: por qué importa para tu arquitectura

    Signal Forms estable no es solo una API nueva: es la culminación de la transición de Angular hacia reactividad explícita y rendimiento previsiblemente escalable. Para proyectos a largo plazo, representa menos boilerplate, menor riesgo de fugas de memoria y una integración natural con la estrategia Zoneless.

    Prepara tu código hoy —desacopla, prueba y adopta interoperabilidad con toSignal— y tu equipo tendrá una migración suave cuando Angular 22 estabilice Signal Forms. La mejora no será estética: será tangible en rendimiento y mantenibilidad.

    Fuentes y recursos

    FAQ

    Respuesta: ¿Qué es Signal Forms?

    Signal Forms expone el estado del formulario —valor, validez, touched/dirty— como Signals nativos en lugar de Observables, permitiendo lecturas síncronas y derivaciones con computed().

    Respuesta: ¿Cuándo debería considerar migrar a Signal Forms?

    Considéralo cuando busques reducir suscripciones manuales, eliminar desfases en validaciones cruzadas, o al adoptar una arquitectura Zoneless para mejoras de rendimiento predecible.

    Respuesta: ¿Cómo afecta Signal Forms a las validaciones cruzadas?

    El runtime de Signals realiza dependency tracking, por lo que las validaciones que dependen de múltiples campos solo se reevaluarán cuando cambien esas dependencias, evitando emisiones redundantes y operadores adicionales de RxJS.

    Respuesta: ¿Puedo mezclar ReactiveForms y Signal Forms?

    Sí. Antes de la estabilización completa, una estrategia pragmática es convertir valueChanges a Signal con toSignal() (rxjs-interop) para interoperabilidad temporal.

    Respuesta: ¿Qué beneficios de rendimiento puedo esperar?

    Menos re-renderings innecesarios, menos trabajo en eventos de alta frecuencia y menor riesgo de fugas por manejo de suscripciones. La integración Zoneless permite re-rendering quirúrgico de los nodos que dependen de un Signal.

    Respuesta: ¿Qué precauciones antes de activar en producción?

    Asegura lógica desacoplada (validadores fuera del FormGroup), cobertura de pruebas para validación y envíos, y observabilidad en producción para comparar métricas (TTI, LCP).

  • Aprende a tipar correctamente props, hooks y contextos en TypeScript y React

    Aprende a tipar correctamente props, hooks y contextos en TypeScript y React

    TypeScript + React: cómo tipar correctamente props, hooks y contextos

    Tiempo estimado de lectura: 4 min

    • Tipado explícito evita errores silenciosos: evita atajos como as any y prefiere contratos claros.
    • Props y refs: evita React.FC, usa referencias DOM con null inicial y valores mutables con valor inicial.
    • Contextos seguros: inicializa con null y expón hooks que hagan fail-fast si se usan fuera del provider.
    • Handlers y hooks: aprovecha los tipos de React (ChangeEvent, FormEvent) y deja que TS infiera cuando sea seguro.

    ¿Quieres dejar de parchear bugs con as any y que tu base de código deje de tener sorpresas en producción? Bien. Esto es lo que realmente necesitas saber sobre TypeScript + React: cómo tipar correctamente props, hooks y contextos. No es teoría. Son patrones que evitan errores silenciosos, mejoran el autocompletado y hacen que el código sea mantenible cuando el equipo crece.

    Resumen rápido (lectores con prisa)

    Tipar React con TypeScript reduce errores en producción y mejora DX. Evita React.FC, inicializa contextos con null y valida con hooks, usa refs con null para DOM y valores iniciales para mutables, y aprovecha los tipos sintéticos de eventos de React.

    Evita React.FC: tipa los parámetros explícitamente

    React.FC fue útil en tutoriales, pero introduce problemas: children implícitos, genéricos torpes y ruido. Tipar la función es más claro y explícito.

    interface ButtonProps {
      label: string;
      onClick: () => void;
      variant?: 'primary' | 'secondary';
      children?: React.ReactNode;
    }
    
    export function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
      return <button className={`btn-${variant}`} onClick={onClick}>{children ?? label}</button>;
    }
    

    React.ReactNode cubre todo lo que necesitas para children. Punto.

    useState: deja que TS infiera cuando pueda, explícito cuando haga falta

    Si el estado empieza con un primitivo, no especifiques el tipo. Si empieza vacío y luego será un objeto, usa una unión con null.

    interface User { id: string; email: string; }
    
    const [count, setCount] = useState(0);            // OK, inferido
    const [user, setUser] = useState<User | null>(null); // OK, explícito
    

    ¿Por qué? Porque evitarás tener que castear más adelante y te proteges contra undefined al acceder a propiedades.

    useRef: dos usos, dos reglas

    useRef sirve para referencias DOM y para valores mutables que no disparan re-render. Los tipos cambian según el valor inicial.

    • DOM refs: inicializa con null y maneja optional chaining.
    • Valores mutables: inicializa con el valor y muta .current.
    const inputRef = useRef<HTMLInputElement | null>(null);
    const renderCount = useRef(0);
    
    inputRef.current?.focus();
    renderCount.current += 1;
    

    No uses as para saltarte el null check. Esa falsedad te estallará en runtime.

    Eventos del DOM: tipa cada handler

    No uses any. React expone tipos sintéticos bien definidos. Úsalos y disfruta del autocompletado.

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      console.log(e.target.value);
    };
    
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
    };
    

    Esto evita errores tontos como leer propiedades inexistentes.

    useContext: seguro, explícito y con fail-fast

    No hagas createContext({} as ThemeContext). Ese as silencia al compilador y deja el error para producción.

    Patrón correcto: contexto con null y hook personalizado que comprueba la presencia del provider.

    interface ThemeContextType { theme: 'light'|'dark'; toggle: () => void; }
    const ThemeContext = createContext<ThemeContextType | null>(null);
    
    export function useTheme() {
      const ctx = useContext(ThemeContext);
      if (!ctx) throw new Error('useTheme debe usarse dentro de ThemeProvider');
      return ctx;
    }
    

    Fail-fast: si alguien usa el hook fuera del provider, fallas rápido y el stack trace dice dónde.

    forwardRef: firma invertida, atención al tipo genérico

    La firma de forwardRef es contraintuitiva: el primer genérico es el tipo de ref, el segundo las props.

    interface InputProps { label: string }
    
    export const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ label, ...props }, ref) => (
      <label>
        {label}
        <input ref={ref} {...props} />
      </label>
    ));
    CustomInput.displayName = 'CustomInput';
    

    Siempre define displayName para facilitar debugging en React DevTools.

    Tips prácticos que cambian proyectos

    • Exporta type con export type cuando sean solo contratos. Eso deja claro que no hay runtime.
    • No uses as para “callar” al compilador. Es un atajo que se vuelve deuda.
    • Si necesitas tipos genéricos en componentes, tipa explícitamente props y evita React.FC.
    • Para APIs y carga asíncrona, combina Zod (o similar) con z.infer si necesitas validación runtime y tipos derivados.

    Checklist rápido antes de push

    • ¿Contextos inicializados con null y validados por hooks? ✔
    • ¿useRef con null para DOM y con valor inicial para mutables? ✔
    • ¿Handlers con React.ChangeEvent / FormEvent? ✔
    • ¿No hay as any salvo casos documentados? ✔

    Cierra con criterio

    Tipar React no es un ejercicio académico. Es la forma más barata de prevenir fallos en producción y mejorar el DX de tu equipo. Haz estas tres cosas hoy:

    1. Revisa contextos: elimina as y añade hooks defensivos.
    2. Estándariza useRef y useState según lo explicado.
    3. Añade displayName a los componentes con forwardRef.

    Aplica esto en tu repo. Si algo rompe después, sabrás exactamente por qué. Esto no acaba aquí. Hay más patrones (componentes polimórficos, inferencia con generics, overloads en hooks) que merecen otra nota.

    FAQ

    ¿Por qué evitar React.FC?

    Porque introduce children implícitos, dificulta genéricos y añade ruido. Tipar explícitamente los parámetros es más claro y evita sorpresas.

    ¿Cuándo especificar el tipo en useState?

    No lo especifiques si el estado inicia con un primitivo (deja que TS infiera). Si el estado inicia vacío y luego será un objeto, usa una unión con null (por ejemplo User | null).

    ¿Cómo tipar correctamente useRef para DOM?

    Inicializa la ref con null y usa el tipo del elemento: useRef<HTMLInputElement | null>(null). Accede con optional chaining (inputRef.current?.focus()).

    ¿Qué hacer si alguien usa un context fuera del provider?

    Exponer un hook que haga fail-fast: si el contexto es null, lanzar un error claro (por ejemplo throw new Error('useTheme debe usarse dentro de ThemeProvider')).

    ¿Es aceptable usar as en alguna situación?

    Evita as salvo casos documentados y justificables. Usarlo para “callar” al compilador oculta problemas que aparecerán en runtime.

    ¿Cómo mejorar la validación de APIs y mantener tipos?

    Combina validación runtime con librerías como Zod y usa z.infer para derivar tipos TypeScript a partir de los esquemas de validación.