Contratos tipificados y protectores Zod para un panel de análisis en tiempo real por WebSocket
Cómo crear un panel de control en tiempo real confiable en TypeScript: definir contratos de carga, validar mensajes WebSocket con Zod y evitar conexiones duplicadas.
Una solicitud de “números en tiempo real, ahora mismo” parece ser un problema de visualización de gráficos, pero en realidad se trata principalmente de un problema de confianza en los datos. Cuando las métricas llegan como JSON sin tipado, cuando dos puntos finales denominan el mismo campo de manera diferente y cuando los sockets se reconectan en bucles, el panel de control parece estar activo pero nadie lo cree. Esta guía sigue un pequeño panel de análisis en tiempo real creado con TypeScript y muestra los patrones que lo hacen fiable: un contrato tipado, validación en el límite del socket, una conexión protegida y un diseño deliberadamente sencillo.
Por qué la versión sin tipado no podía ser confiable
Considere un punto de partida típico: un panel administrativo a medio terminar escrito en JavaScript informal. Los síntomas son familiares:
- Los valores fluyen a través del código como
any, por lo que el editor no ofrece ninguna ayuda. - La biblioteca de gráficos recibe cualquier formato que el servidor haya enviado por casualidad.
user, otra users_count; se trata de conceptos similares.NaN aparece en la interfaz de usuario cada vez que un campo falta o está mal formado.Ninguno de estos es un error exótico. Comparten una misma causa raíz: no existe un contrato acordado entre la fuente de datos y la interfaz de usuario. La solución es una única regla que todo el equipo puede aplicar: si la estructura de los datos no está tipada ni verificada, no llegará a la interfaz de usuario.
Diseñar un panel de control que realmente utilicen las personas
Los paneles de control con aspecto impresionante y los que son realmente útiles rara vez son lo mismo. Una primera versión básica podría incluir solo:
- La cantidad de visitantes en este momento
- La tasa de conversión en las últimas 24 horas
- Las páginas más visitadas
- La tasa de errores actual
- Un indicador de “última actualización” para que los usuarios sepan que los datos están actualizados
La pila mantiene una focalización constante:
- Next.js con el App Router
- TypeScript en modo estricto
- Recharts para los gráficos
- WebSockets para enviar actualizaciones
- Zod para validar cada carga de datos antes de que React la vea
El objetivo no es un producto perfecto, sino un conjunto de cifras sobre las que el equipo deja de discutir.
Escribir primero el contrato de datos
En lugar de obtener JSON con la esperanza de que coincida, comience describiendo exactamente qué espera la interfaz de usuario. Los tipos a continuación abarcan una métrica genérica (con su porcentaje de cambio y una marca de tiempo ISO) así como la carga de datos completa que entrega el socket.
type DashboardMetric = {
id: string;
label: string;
value: number;
deltaPercent: number;
updatedAt: string; // ISO
};
type LiveDashboardPayload = {
visitorsNow: number;
conversionRate: number;
topPages: Array<{ path: string; views: number }>;
errorRate: number;
metrics: DashboardMetric[];
};
Estos tipos documentan la intención y ofrecen completado automático, pero desaparecen en tiempo de ejecución. Un mensaje de WebSocket es simplemente una cadena, y TypeScript no puede verificar lo que envía el servidor. Por eso es importante el siguiente paso.
Validación de cada mensaje de socket con Zod
El esquema de Zod refleja el contrato y agrega reglas que los tipos no pueden expresar: las cantidades no pueden ser negativas, las vistas de página deben ser números enteros, y las tasas de conversión y error son fracciones entre 0 y 1. El campo updatedAt debe ser una cadena de fecha y hora válida.
import { z } from "zod";
const LiveDashboardSchema = z.object({
visitorsNow: z.number().nonnegative(),
conversionRate: z.number().min(0).max(1),
topPages: z.array(
z.object({
path: z.string(),
views: z.number().int().nonnegative(),
})
),
errorRate: z.number().min(0).max(1),
metrics: z.array(
z.object({
id: z.string(),
label: z.string(),
value: z.number(),
deltaPercent: z.number(),
updatedAt: z.string().datetime(),
})
),
});
Con esto en vigor, un payload mal formado ya no hace que la página se caiga ni introduce valores NaN en un gráfico. Es rechazado y el último estado válido permanece en la pantalla.
Mantener tanto los tipos manuscritos como el esquema conduce a desviaciones. Una mejora común consiste en tratar el esquema como la fuente de verdad y derivar el tipo con z.infer<typeof LiveDashboardSchema>. También verifique su versión de Zod: las versiones recientes ofrecen z.iso.datetime() como la forma preferida para verificar fechas y horas, así que confirme la API según la documentación actual. Para conocer más sobre cómo compartir un mismo esquema entre diferentes capas, consulte el uso de un único esquema Zod en el frontend y backend.
Controlar las reconexiones y los listeners duplicados
Las funcionalidades en tiempo real suelen fallar de una manera específica. Una primera versión ingenua se reconecta sin cesar, agrega un nuevo manejador de mensajes en cada intento, apila actualizaciones de gráficos sobre las antiguas y, eventualmente, ralentiza al navegador.
La solución consiste en considerar la conexión como una pequeña máquina de estados: idle, luego connecting, después live; pasa a reconnecting y vuelve a live cuando la red se recupera. La regla más importante es que solo puede existir un socket a la vez. La función connect que se muestra a continuación la aplica: si ya hay un socket abierto o en proceso de apertura, devuelve inmediatamente. Los mensajes entrantes se analizan con safeParse, el cual devuelve un objeto de resultado en lugar de lanzar una excepción, de modo que los datos inválidos se registran y se omiten, mientras que los datos válidos actualizan el estado.
let socket: WebSocket | null = null;
function connect() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
socket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL!);
socket.onmessage = (event) => {
const parsed = LiveDashboardSchema.safeParse(JSON.parse(event.data));
if (!parsed.success) {
console.warn("Invalid live payload", parsed.error);
return;
}
setDashboard(parsed.data);
};
}
Hay algunas lagunas que conviene solucionar antes de la producción. JSON.parse puede lanzar un error con un formato que no sea JSON, por lo que hay que envolverlo en try/catch. El fragmento muestra la protección, pero no el camino para reconectar; añada un manejo de onclose con un retraso de retroceso para que una interrupción del servidor no active un bucle de reconexión excesivo. En React, cierre el socket en la limpieza de efectos para que los recargues (incluida la doble invocación de efectos en el Modo Estricto de desarrollo) no provoquen fugas de conexiones.
Diseñando para la pregunta “¿dónde miro primero?”
Es tentador decorar un panel en tiempo real con degradados, tarjetas brillantes y muchos colores. Una mejor prueba es preguntarle al stakeholder a dónde deben dirigirse primero sus ojos, y luego eliminar todo lo que no responda a esa pregunta. Un diseño que funcione bien es:
- Una fila con un máximo de cuatro métricas principales
En vivo • actualizado hace 2 sTambién ayuda escribir el texto manualmente. Cuando cada métrica tiene una forma definida, la interfaz no puede generar widgets ad hoc para datos que nadie especificó. Estas restricciones mantienen un diseño honesto.
Lo que notan los usuarios después del lanzamiento
Una vez que se lanza un panel de control como este, los comentarios rara vez se refieren a la arquitectura. La gente dice que finalmente confía en las cifras, que la página ya no se congela y se sorprende al ver que realmente está en tiempo real. Ese es el verdadero propósito de un panel de control: no una galería de gráficos, sino una herramienta en la que las personas puedan confiar durante una reunión.
Puntos clave
- Escriba los límites y valide cada carga externa en tiempo de ejecución; TypeScript por sí solo no puede ver lo que envía el servidor.
- Mantenga activo el modo estricto; siempre da buenos resultados cada vez que se modifica el código.
- Trate una conexión en tiempo real como una máquina de estados y permita exactamente un socket.
- Retire los elementos de la interfaz hasta que la idea principal quede clara.
- Prefiera una vista sencilla con datos correctos a una vista pulida con datos dudosos.
Si está creando su primer panel de control en tiempo real, evite comenzar con algo demasiado grande. Comience con un único paquete de datos validado mediante un esquema Zod, muestre tres números junto con una marca de tiempo de actualización, y añada sockets solo una vez que esa base esté establecida.
Lecturas relacionadas
- Separando las capas de dominio, datos e interfaz en un código base de Next.js App Router — Un estudio de caso de Pokédex que muestra cómo dividir una aplicación Next.js App Router en capas de dominio, datos y presentación utilizando Prisma, Zod, autenticación por cookies y caché.
- SEO técnico en Next.js App Router: Metadatos, sitemaps y JSON-LD — Aprenda cómo los ayudantes de metadatos compartidos, las configuraciones predeterminadas del layout raíz, robots.ts, un sitemap dinámico, JSON-LD preciso y las auditorías de páginas proporcionan a una aplicación Next.js una base sólida para el SEO.
- Vue 3 en la práctica: Composables, contratos tipados y estado proporcional — Descubra cómo la API de composición de Vue 3, los props y emits tipados, Pinia y una adopción incremental permiten que una aplicación crezca solo en la medida de lo necesario, y cuándo Vue no es la opción adecuada.