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.tsregroupe 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 pourevents: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.tsdéfinit un contexte partagé (incluantuseretsessionpour l'authentification), etmiddlewares.tsapplique des vérifications comme l'authentification. Le serveur Hono intègre oRPC via un gestionnaire (RPCHandler) dansindex.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 :
// 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 :
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), etisLoading(é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
idest défini.
Ces hooks sont utilisés dans les composants, comme home.tsx ou event.tsx, pour afficher les données dynamiquement. Par exemple :
// 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
- Créez un fichier
apps/web/src/rpc/procedures/stats.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 };- Ajoutez la procédure au routeur dans
apps/web/src/rpc/router.ts:
import { stats } from "./procedures/stats.js";
export const router = {
events,
locations,
categories,
organizers,
stats,
};- 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 :
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 :
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
- Lancez le backend (
bun run dev:server) et le frontend (bun run dev:web). - Ouvrez
http://localhost:5173et vérifiez que les statistiques s'affichent. - Vérifiez la console réseau pour confirmer que la requête
/rpc/stats/getEventStatsest envoyée.
Bonnes pratiques
- Validation des entrées : Utilisez
zoddans 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
errorde SWR. - Mise en cache : Exploitez les fonctionnalités de SWR comme
revalidateOnFocusourefreshIntervalpour des données dynamiques. - Types partagés : Assurez-vous que
packages/sharedest toujours à jour après avoir modifié le routeur backend. - Nommage des hooks : Utilisez des noms comme
useEntityouuseEntityByIdpour une cohérence danshooks/.