Accueil / Articles / Contrats tapés et gardiens Zod pour un tableau de bord d’analyse WebSocket en temps réel

Contrats tapés et gardiens Zod pour un tableau de bord d’analyse WebSocket en temps réel

Comment créer un tableau de bord en temps réel fiable en TypeScript : définir des contrats de charge utile, valider les messages WebSocket avec Zod, et empêcher les connexions dupliquées.

1298 mots

Une demande de « nombres en temps réel, maintenant » semble être un problème lié aux graphiques, mais il s’agit principalement d’un problème de confiance dans les données. Lorsque les métriques arrivent sous forme de JSON non typé, lorsque deux points d’entrée donnent des noms différents au même champ et lorsque les sockets se reconnectent en boucle, le tableau de bord semble actif mais personne n’y croit. Cette présentation suit un petit tableau de bord d’analyse en temps réel construit avec TypeScript et montre les principes qui en assurent la fiabilité : un contrat typé, une validation au niveau des sockets, une connexion sécurisée et un layout volontairement simple.

Pourquoi la version non typée ne pouvait pas être fiable

Considérons un point de départ typique : un panneau d’administration inachevé écrit en JavaScript brut. Les symptômes sont familiers :

  • Les valeurs circulent dans le code sous la forme de any, ce qui empêche l’éditeur d’offrir une aide utile.
  • La bibliothèque de graphiques reçoit la forme que le serveur a envoyée par hasard.
  • Une API renvoie user, une autre users_count, pour des concepts similaires.
  • NaN apparaît dans l’interface utilisateur chaque fois qu’un champ est manquant ou mal formaté.
  • Aucun de ces problèmes n’est particulièrement complexe. Ils ont tous une même cause racine : il n’existe pas de contrat clair entre la source de données et l’interface utilisateur. La solution consiste en une règle simple que toute l’équipe peut appliquer : si la structure des données n’est pas définie et vérifiée, elle ne parvient pas à l’interface utilisateur.

    Définir le périmètre d’un tableau de bord réellement utilisé

    Les tableaux de bord au design impressionnant et ceux qui sont utiles ne correspondent que rarement. Une première version simplifiée pourrait inclure uniquement :

    1. Le nombre de visiteurs en ce moment
    2. Taux de conversion au cours des 24 dernières heures
    3. Les pages les plus consultées
    4. Le taux d’erreur actuel
    5. Un indicateur « dernière mise à jour » pour que les utilisateurs sachent que les données sont à jour

    La pile technologique reste uniformément ciblée :

    • Next.js avec l’App Router
    • TypeScript en mode strict
    • Recharts pour les graphiques
    • WebSockets pour envoyer des mises à jour
    • Zod pour valider chaque charge avant qu’elle ne soit traitée par React

    L’objectif n’est pas un produit parfait. Il s’agit d’un ensemble de chiffres sur lesquels l’équipe cesse de se disputer.

    Rédiger d’abord le contrat de données

    Au lieu de récupérer du JSON en espérant qu’il corresponde, commencez par décrire précisément ce que l’interface utilisateur attend. Les types ci-dessous couvrent une métrique générale (avec son pourcentage de variation et une date ISO) ainsi que la charge complète transmise via le 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[];
    };
    

    Ces types documentent l’intention et permettent la complétion automatique, mais ils disparaissent en temps de exécution. Un message WebSocket n’est qu’une chaîne de caractères, et TypeScript ne peut pas vérifier ce que l’serveur envoie. C’est pourquoi l’étape suivante est importante.

    Vérification de chaque message socket avec Zod

    Le schéma Zod reflète le contrat et ajoute des règles que les types ne peuvent pas exprimer : les comptes ne doivent pas être négatifs, le nombre de vues de page doit être un entier, et les taux de conversion ainsi que les taux d’erreur doivent être des fractions comprises entre 0 et 1. Le champ updatedAt doit être une chaîne de date et d’heure valide.

    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(),
        })
      ),
    });
    

    Avec cela en place, un chargement mal formaté ne provoque plus la fermeture de la page ni l’injection de NaN dans un graphique. Il est rejeté et l’état valide précédent reste affiché à l’écran.

    Le maintien à la fois des types manuscrits et du schéma peut entraîner des dérives. Une approche courante consiste à considérer le schéma comme source de vérité et à dériver les types à l’aide de z.infer<typeof LiveDashboardSchema>. Vérifiez également la version de Zod : les versions récentes proposent z.iso.datetime() comme forme préférée pour la vérification des dates et heures, il convient donc de confirmer l’API en consultant la documentation actuelle. Pour en savoir plus sur le partage d’un même schéma entre différentes couches, consultez l’utilisation d’un seul schéma Zod sur le frontend et le backend.

    Gérer les reconnects et les écouteurs dupliqués

    Les fonctionnalités en temps réel ont tendance à échouer de manière spécifique. Une première version naïve se reconnecte indéfiniment, ajoute un nouveau gestionnaire de messages à chaque tentative, empile les mises à jour des graphiques par-dessus celles qui sont obsolètes, ce qui finit par ralentir fortement le navigateur.

    La solution consiste à considérer cette connexion comme une petite machine d’état : idle, puis connecting, ensuite live ; elle passe à reconnecting et revient à live lorsque le réseau se rétablit. La règle la plus importante est qu’un seul socket ne peut exister à la fois. La fonction connect ci-dessous la respecte : si un socket est déjà ouvert ou en train de s’ouvrir, elle renvoie immédiatement. Les messages reçus sont analysés avec safeParse, qui renvoie un objet de résultat au lieu de lancer une erreur, de sorte que les données invalides sont enregistrées et ignorées tandis que les données valides mettent à jour l’état.

    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);
      };
    }
    

    Quelques lacunes doivent être comblées avant la mise en production. JSON.parse peut lui-même lancer une erreur en cas de données non JSON, il convient donc de le placer dans un bloc try/catch. Le fragment montre la protection contre les erreurs mais pas la procédure de réconnexion ; ajoutez un traitement onclose avec un délai de retentissement afin qu’une panne du serveur ne provoque pas une boucle de réconnexion incessante. Dans React, fermez le socket lors du nettoyage des effets afin que les remontages (y compris les appels doubles des effets en mode Strict Mode de développement) ne fassent pas fuir les connexions.

    Concevoir en répondant à la question « Où regarde-t-on en premier ? »

    Il est tentant de décorer un tableau de bord en temps réel avec des dégradés, des cartes lumineuses et de nombreuses couleurs. Un meilleur test consiste à demander aux parties prenantes où leurs yeux doivent se poser en premier, puis à supprimer tout ce qui ne répond pas à cette question. Un layout efficace est :

    • Une rangée de quatre indicateurs principaux au maximum
  • Un graphique principal
  • Une table
  • Une seule ligne d’état telle que En direct • mis à jour il y a 2 s
  • La saisie manuelle est également utile ici. Lorsque chaque métrique a une forme définie, l’interface ne peut pas générer de widgets ad hoc pour des données que personne n’a spécifiées. Ces contraintes maintiennent un design honnête.

    Ce que les utilisateurs remarquent après le lancement

    Lorsqu’un tableau de bord comme celui-ci est mis en ligne, les retours concernent rarement l’architecture. Les utilisateurs disent qu’ils font enfin confiance aux chiffres, que la page ne fige plus, et ils sont surpris de constater qu’elle est vraiment en temps réel. Telle est la véritable fonction d’un tableau de bord : pas une galerie de graphiques, mais un outil sur lequel on peut compter lors d’une réunion.

    Points clés

    • Saisissez les limites et validez chaque charge externe en temps réel ; TypeScript seul ne peut pas voir ce que l’serveur envoie.
    • Gardez le mode strict activé ; cela s’avère utile à chaque modification du code.
    • Considérez une connexion en temps réel comme une machine à états et autorisez un seul socket.
    • Retirez les éléments d’interface jusqu’à ce que l’essentiel soit clair.
    • Préférez une vue simple avec des données fiables à une vue élaborée mais contenant des données douteuses.

    Si vous créez votre premier tableau de bord en temps réel, résistez à l’envie de commencer grand. Commencez par un seul envoi de données vérifié à l’aide d’un schéma Zod, affichez trois chiffres accompagnés d’une date de fraîcheur, et n’intégrez les sockets qu’une fois cette base en place.

    Lectures complémentaires