Skip to content

Utilisation de l'API unifiée

Le projet Charly utilise une API unifiée basée sur oRPC (Object-RPC), une bibliothèque qui permet de définir des endpoints backend avec une typisation forte et de les consommer côté frontend de manière transparente. Cette approche garantit une communication sécurisée et typée entre le backend et le frontend, en s'appuyant sur des types partagés via le package @charly/shared et @charly/db/zod. Les hooks frontend, construits avec SWR, simplifient la gestion des requêtes et la mise en cache des données. Cette page explique le fonctionnement de l'API, comment les types sont partagés, et comment les hooks sont conçus, avec un tutoriel rapide pour illustrer ces concepts.

Fonctionnement d'oRPC côté backend

oRPC est utilisé dans apps/backend pour définir des endpoints API sous forme de procédures (ou "handlers") organisées dans un routeur. Ces procédures sont définies dans le dossier apps/web/src/rpc/procedures/ et exposées via un routeur central (apps/web/src/rpc/router.ts). Voici comment oRPC est configuré :

  • Routeur central : Le fichier router.ts regroupe les procédures pour les entités principales (events, locations, categories, organizers). Exemple :

    ts
    // apps/web/src/rpc/router.ts
    import { categories } from "./procedures/categories.js";
    import { events } from "./procedures/events.js";
    import { locations } from "./procedures/locations.js";
    import { organizers } from "./procedures/organizers.js";
    
    export const router = {
      events,
      locations,
      categories,
      organizers,
    };
  • Procédures : Chaque procédure définit une requête (ex. getAll, getById) avec une validation d'entrée via Zod et une logique métier. Exemple pour events :

    ts
    // apps/web/src/rpc/procedures/events.ts
    import { db } from "@charly/db";
    import { base } from "../context.js";
    import { events as eventsTable } from "@charly/db/schema/events.ts";
    import { z } from "zod";
    import { eq } from "drizzle-orm";
    
    const getAll = base.handler(async () => {
      try {
        const result = await db.select().from(eventsTable).execute();
        return result;
      } catch (error) {
        throw new Error("Failed to fetch events");
      }
    });
    
    const getEventById = base
      .input(z.object({ id: z.string().uuid() }))
      .handler(async (req) => {
        const { id } = req.input;
        const result = await db
          .select()
          .from(eventsTable)
          .where(eq(eventsTable.id, id))
          .limit(1)
          .execute();
        if (result.length === 0) {
          throw new Error("Event not found");
        }
        return result[0];
      });
    
    export const events = { getAll, getEventById };
  • Middleware et contexte : Le fichier context.ts définit un contexte partagé (incluant user et session pour l'authentification), et middlewares.ts applique des vérifications comme l'authentification. Le serveur Hono intègre oRPC via un gestionnaire (RPCHandler) dans index.ts.

  • Exposition : Les endpoints sont accessibles sous /rpc/* (ex. /rpc/events/getAll). Hono gère les requêtes HTTP, et oRPC transforme les appels en exécutions de fonctions typées.

Consommation de l'API côté frontend

Le frontend (apps/web) consomme l'API via des hooks personnalisés définis dans src/hooks/, qui utilisent le client oRPC pour envoyer des requêtes. Les types communs sont partagés via le package @charly/shared, garantissant une typisation cohérente.

Partage des types

Dans apps/web, le client oRPC est configuré pour utiliser ce type :

ts
// apps/web/src/lib/oRPC.ts
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
import { SERVER_URL } from "@/data/settings";
import { createORPCReactQueryUtils } from "@orpc/react-query";
import { router } from "@charly/shared/types";
import { RouterClient } from "@orpc/server";

export const link = new RPCLink({
  url: `${SERVER_URL}/rpc`,
});

const client: RouterClient<typeof router> = createORPCClient(link);
export const orpc = createORPCReactQueryUtils(client);

Le client oRPC est utilisé dans les hooks pour appeler les procédures backend, comme oRPC.events.getAll().

Conception des hooks avec SWR

Les hooks frontend, comme useEvents ou useEvent, utilisent SWR pour gérer les requêtes, la mise en cache, et les revalidations. SWR est une bibliothèque légère qui simplifie la récupération de données asynchrones avec des fonctionnalités comme le cache, les revalidations automatiques, et les états de chargement.

Exemple de hook dans apps/web/src/hooks/use-events.ts :

ts
import { useORPC } from "./use-orpc";
import useSWR from "swr";

export const useEvent = (id: string | undefined) => {
  const orpc = useORPC();
  return useSWR(id ? orpc.events.getEventById.key() : null, () =>
    id ? orpc.events.getEventById.call({ id }) : null
  );
};
  • Clés SWR : La clé identifie la requête dans le cache. Elle est passée à une fonction qui appelle oRPC.events.getEventById().
  • Retour : Le hook retourne data (les événements), error (erreurs éventuelles), et isLoading (état de chargement) et d'autres choses.
  • Mise en cache : SWR met en cache les résultats, réduisant les requêtes réseau inutiles.
  • Requête conditionnelle : La requête n'est exécutée que si id est défini.

Ces hooks sont utilisés dans les composants, comme home.tsx ou event.tsx, pour afficher les données dynamiquement. Par exemple :

tsx
// apps/web/src/pages/event.tsx
import { useEvent } from "@/hooks/use-event";

function Event() {
  const { data: event, isLoading } = useEvent(id);

  if (isLoading) return <div>Loading...</div>;
  if (!event) return <div>Event not found</div>;

  return <h1>{event.name}</h1>;
}

Tutoriel rapide : Créer et consommer un endpoint oRPC

Ce tutoriel montre comment ajouter un nouvel endpoint oRPC pour récupérer des statistiques d'événements et l'utiliser dans le frontend avec un hook SWR.

Étape 1 : Ajouter une procédure backend

  1. Créez un fichier apps/web/src/rpc/procedures/stats.ts :
ts
import { db } from "@charly/db";
import { base } from "../context.js";
import { events as eventsTable } from "@charly/db/schema/events.ts";

const getEventStats = base.handler(async () => {
  try {
    const totalEvents = await db
      .select({ count: db.fn.count() })
      .from(eventsTable)
      .execute();
    const publishedEvents = await db
      .select({ count: db.fn.count() })
      .from(eventsTable)
      .where(eq(eventsTable.status, "published"))
      .execute();
    return {
      total: totalEvents[0].count,
      published: publishedEvents[0].count,
    };
  } catch (error) {
    throw new Error("Failed to fetch event stats");
  }
});

export const stats = { getEventStats };
  1. Ajoutez la procédure au routeur dans apps/web/src/rpc/router.ts :
ts
import { stats } from "./procedures/stats.js";

export const router = {
  events,
  locations,
  categories,
  organizers,
  stats,
};
  1. Redémarrez le backend (bun run dev:server) pour exposer l'endpoint /rpc/stats/getEventStats.

Étape 2 : Créer un hook frontend

Créez un fichier apps/web/src/hooks/use-event-stats.ts :

ts
import useSWR from "swr";
import { orpc } from "../lib/orpc";

export function useEventStats() {
  return useSWR(
    orpc.stats.getEventStats.key(),
    () => orpc.stats.getEventStats.call
  );
}

Étape 3 : Utiliser le hook dans un composant

Ajoutez le hook à un composant, par exemple dans apps/web/src/pages/home.tsx :

tsx
import { useEventStats } from "@/hooks/use-event-stats";

function Home() {
  const { data: stats, isLoading } = useEventStats();

  if (isLoading) return <div>Loading stats...</div>;

  return (
    <div>
      <p>Total events: {stats?.total}</p>
      <p>Published events: {stats?.published}</p>
    </div>
  );
}

Étape 4 : Tester

  1. Lancez le backend (bun run dev:server) et le frontend (bun run dev:web).
  2. Ouvrez http://localhost:5173 et vérifiez que les statistiques s'affichent.
  3. Vérifiez la console réseau pour confirmer que la requête /rpc/stats/getEventStats est envoyée.

Bonnes pratiques

  • Validation des entrées : Utilisez zod dans les procédures oRPC pour valider les paramètres (ex. z.string().uuid() pour les IDs).
  • Gestion des erreurs : Retournez des erreurs claires dans les procédures et gérez-les dans les hooks avec error de SWR.
  • Mise en cache : Exploitez les fonctionnalités de SWR comme revalidateOnFocus ou refreshInterval pour des données dynamiques.
  • Types partagés : Assurez-vous que packages/shared est toujours à jour après avoir modifié le routeur backend.
  • Nommage des hooks : Utilisez des noms comme useEntity ou useEntityById pour une cohérence dans hooks/.