Exemple pratique
Construisons quelque chose de concret : charger un tableau de bord utilisateur depuis une API, valider les données, les transformer et gérer les erreurs proprement.
Cet exemple combine :
-
Zygos - Opérations asynchrones sûres avec
ResultAsync - Kanon - Validation de schémas
- Arkhe - Utilitaires de transformation de données
Le scénario
Vous devez charger les données du tableau de bord d'un utilisateur depuis une API. La réponse peut être malformée, le réseau peut échouer, et vous devez transformer les données brutes avant de les afficher.
Approche traditionnelle : try/catch imbriqués, validation manuelle, croiser les doigts.
Approche Pithos : composable, type-safe, élégante.
Étape 1 : Définir vos schémas
D'abord, définissez à quoi ressemblent des données valides avec Kanon :
// src/lib/schemas.ts
import {
object,
string,
number,
boolean,
array,
optional,
parse,
} from "pithos/kanon";
// Définir la structure attendue de la réponse API
const UserSchema = object({
id: string(),
firstName: string(),
lastName: string(),
email: string().email(),
role: string(),
createdAt: string(),
preferences: optional(
object({
theme: optional(string()),
language: optional(string()),
notifications: optional(boolean()),
})
),
});
const PostSchema = object({
id: string(),
title: string(),
content: string(),
publishedAt: optional(string()),
status: string(),
});
const DashboardSchema = object({
user: UserSchema,
posts: array(PostSchema),
stats: object({
totalViews: number(),
totalLikes: number(),
totalComments: number(),
}),
});
Si vous venez de Zod, le shim compatible Zod offre une syntaxe familière avec moins d'imports, au prix de bundles légèrement plus gros :
import { z } from "pithos/kanon/helpers/as-zod.shim";
const UserSchema = z.object({
id: z.string(),
firstName: z.string(),
email: z.string().email(),
// ... même API Zod
});
Étape 2 : Créer des helpers API sûrs
Encapsulez les appels fetch avec Zygos pour une gestion d'erreurs sûre :
// src/lib/api.ts
import {
ResultAsync,
errAsync,
okAsync,
} from "pithos/zygos/result/result-async";
// Créer un wrapper fetch sûr
const safeFetch = ResultAsync.fromThrowable(
fetch,
(error) => `Erreur réseau : ${error}`
);
// Créer un parser JSON sûr
const safeJson = <T>(response: Response) =>
ResultAsync.fromThrowable(
async () => (await response.json()) as T,
(error) => `Erreur de parsing JSON : ${error}`
)();
Étape 3 : Ajouter la transformation de données
Utilisez les utilitaires Arkhe pour transformer les données validées :
// src/lib/transformers.ts
import { groupBy } from "pithos/arkhe/array/group-by";
import { capitalize } from "pithos/arkhe/string/capitalize";
type User = {
id: string;
firstName: string;
lastName: string;
email: string;
role: string;
createdAt: string;
preferences?: {
theme?: string;
language?: string;
notifications?: boolean;
};
};
type Post = {
id: string;
title: string;
content: string;
publishedAt?: string;
status: string;
};
// Transformer les données utilisateur pour l'affichage
function formatUser(user: User) {
return {
id: user.id,
fullName: `${capitalize(user.firstName)} ${capitalize(user.lastName)}`,
email: user.email,
role: capitalize(user.role),
preferences: user.preferences ?? {
theme: "light",
language: "en",
notifications: true,
},
};
}
// Transformer les articles pour le tableau de bord
function formatPosts(posts: Post[]) {
const grouped = groupBy(posts, (post) => post.status);
return {
published: grouped["published"] ?? [],
draft: grouped["draft"] ?? [],
total: posts.length,
};
}
Étape 4 : Tout composer ensemble
Combinez maintenant toutes les pièces en un seul pipeline composable :
// src/lib/api.ts (suite)
type DashboardData = {
user: ReturnType<typeof formatUser>;
posts: ReturnType<typeof formatPosts>;
stats: {
totalViews: number;
totalLikes: number;
totalComments: number;
};
};
function loadDashboard(userId: string): ResultAsync<DashboardData, string> {
return safeFetch(`/api/dashboard/${userId}`)
.andThen((response) => {
if (!response.ok) {
return errAsync(`Erreur HTTP : ${response.status}`);
}
return okAsync(response);
})
.andThen((response) => safeJson<unknown>(response))
.andThen((data) => {
const result = parse(DashboardSchema, data);
if (!result.success) {
return errAsync(`Données invalides : ${result.error}`);
}
return okAsync(result.data);
})
.map((data) => ({
user: formatUser(data.user),
posts: formatPosts(data.posts),
stats: data.stats,
}));
}
Étape 5 : L'utiliser dans votre app
Avec le pipeline en place, il ne reste qu'à consommer le résultat : on vérifie succès ou erreur, puis on affiche.
// src/components/Dashboard.tsx
async function initDashboard() {
const result = await loadDashboard("user-123");
if (result.isErr()) {
// Gérer l'erreur - afficher un message, réessayer, fallback...
showError(result.error);
return;
}
// TypeScript sait que result.value est DashboardData
const { user, posts, stats } = result.value;
renderHeader(user.fullName, user.role);
renderPostsList(posts.published);
renderDraftsBadge(posts.draft.length);
renderStats(stats);
}
Démo interactive
- schemas.ts
- api.ts
- transformers.ts
- useDashboard.ts
- Dashboard.tsx
/**
* Kanon schemas for validating API responses
* Using Pithos/Kanon v3 for schema validation
*/
import {
object,
string,
number,
boolean,
array,
optional,
} from "pithos/kanon/index";
// Define the expected API response structure
export const UserSchema = object({
id: string(),
firstName: string(),
lastName: string(),
email: string().email(),
role: string(),
createdAt: string(),
preferences: optional(
object({
theme: optional(string()),
language: optional(string()),
notifications: optional(boolean()),
})
),
});
export const PostSchema = object({
id: string(),
title: string(),
content: string(),
publishedAt: optional(string()),
status: string(),
});
export const DashboardSchema = object({
user: UserSchema,
posts: array(PostSchema),
stats: object({
totalViews: number(),
totalLikes: number(),
totalComments: number(),
}),
});
/**
* Safe API helpers using Zygos ResultAsync
*
* This file demonstrates the Zygos pattern for type-safe async operations.
* In this example, we use mock data, but the pattern works identically with real APIs.
*/
import { ResultAsync, errAsync, okAsync } from "pithos/zygos/result/result-async";
import { parse } from "pithos/kanon/index";
import { DashboardSchema } from "./schemas";
import { formatUser, formatPosts } from "./transformers";
import type { DashboardData, RawDashboardData } from "./types";
/**
* Load dashboard data with full error handling pipeline
*
* This demonstrates the Zygos ResultAsync pattern:
* 1. Wrap async operations in ResultAsync
* 2. Chain with andThen for sequential operations
* 3. Use map for transformations
* 4. All errors are captured and typed
*/
export function loadDashboard(userId: string): ResultAsync<DashboardData, string> {
return ResultAsync.fromPromise(
fetch(`/api/dashboard/${userId}`),
(error) => `Network error: ${error}`
)
.andThen((response) => {
if (!response.ok) {
return errAsync(`HTTP error: ${response.status}`);
}
return ResultAsync.fromPromise(
response.json() as Promise<unknown>,
(error) => `JSON parse error: ${error}`
);
})
.andThen((data) => {
const result = parse(DashboardSchema, data);
if (!result.success) {
return errAsync(`Invalid data: ${result.error}`);
}
// Safe cast: parse() validates that data matches RawDashboardData structure
return okAsync(result.data as RawDashboardData);
})
.map((data) => ({
user: formatUser(data.user),
posts: formatPosts(data.posts),
stats: data.stats,
}));
}
/**
* Data transformers using Arkhe utilities
*/
import { groupBy } from "pithos/arkhe/array/group-by";
import { capitalize } from "pithos/arkhe/string/capitalize";
import type { User, Post, FormattedUser, FormattedPosts } from "./types";
/**
* Transform user data for display
*/
export function formatUser(user: User): FormattedUser {
const defaultPrefs = {
theme: "light",
language: "en",
notifications: true,
};
return {
id: user.id,
fullName: `${capitalize(user.firstName)} ${capitalize(user.lastName)}`,
email: user.email,
role: capitalize(user.role),
preferences: {
theme: user.preferences?.theme ?? defaultPrefs.theme,
language: user.preferences?.language ?? defaultPrefs.language,
notifications: user.preferences?.notifications ?? defaultPrefs.notifications,
},
};
}
/**
* Transform posts for the dashboard, grouped by status
*/
export function formatPosts(posts: Post[]): FormattedPosts {
const grouped = groupBy(posts, (post) => post.status);
return {
published: grouped["published"] ?? [],
draft: grouped["draft"] ?? [],
total: posts.length,
};
}
/**
* Custom hook for loading dashboard data
* Demonstrates the Pithos pipeline in a React context
*/
import { useState, useEffect, useCallback } from "react";
import { parse } from "pithos/kanon/index";
import { DashboardSchema } from "@/lib/schemas";
import { formatUser, formatPosts } from "@/lib/transformers";
import { mockDashboardData } from "@/lib/mock-data";
import type { DashboardData, RawDashboardData } from "@/lib/types";
type DashboardState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: DashboardData }
| { status: "error"; error: string };
/**
* Simulates the full Pithos pipeline with mock data
* In production, this would use the loadDashboard function from api.ts
*/
export function useDashboard(userId: string) {
const [state, setState] = useState<DashboardState>({ status: "idle" });
const load = useCallback(async () => {
setState({ status: "loading" });
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 1000));
try {
// Step 1: Validate with Kanon
const result = parse(DashboardSchema, mockDashboardData);
if (!result.success) {
setState({ status: "error", error: `Validation failed: ${result.error}` });
return;
}
const rawData = result.data as RawDashboardData;
// Step 2: Transform with Arkhe utilities
const dashboardData: DashboardData = {
user: formatUser(rawData.user),
posts: formatPosts(rawData.posts),
stats: rawData.stats,
};
setState({ status: "success", data: dashboardData });
} catch (err) {
setState({ status: "error", error: `Unexpected error: ${err}` });
}
}, [userId]);
useEffect(() => {
load();
}, [load]);
return { state, reload: load };
}
import { useDashboard } from "@/hooks/useDashboard";
import { Header } from "./Header";
import { StatsCards } from "./StatsCards";
import { PostsList } from "./PostsList";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { ErrorDisplay } from "./ErrorDisplay";
import { DemoControls } from "./DemoControls";
export function Dashboard() {
const { state, reload } = useDashboard("user-123");
if (state.status === "idle" || state.status === "loading") {
return <LoadingSkeleton />;
}
if (state.status === "error") {
return <ErrorDisplay error={state.error} onRetry={reload} />;
}
const { user, posts, stats } = state.data;
return (
<div className="min-h-screen bg-background">
<Header user={user} />
<main className="container mx-auto space-y-6 px-4 py-6">
<StatsCards stats={stats} />
<div className="grid gap-6 lg:grid-cols-2">
<PostsList
posts={posts.published}
title="Published Posts"
description="Your live content"
variant="published"
/>
<PostsList
posts={posts.draft}
title="Drafts"
description="Work in progress"
variant="draft"
/>
</div>
{/* Demo controls for testing error handling */}
<DemoControls />
{/* Pithos info card */}
<div className="rounded-lg border bg-muted/50 p-4">
<h3 className="font-semibold">✨ Powered by Pithos</h3>
<p className="mt-1 text-sm text-muted-foreground">
This dashboard uses <strong>Kanon</strong> for schema validation,{" "}
<strong>Arkhe</strong> for data transformation (capitalize, groupBy), and{" "}
<strong>Zygos</strong> patterns for type-safe error handling.
</p>
</div>
</main>
</div>
);
}
La démo ci-dessus est plus complète que les extraits de code : elle est intégrée dans un projet React et inclut l'interface utilisateur.
Le code source complet est disponible sur GitHub.
Ce que vous venez de construire
Avec un minimum de code, vous avez :
Appels API type-safe - response.json() ne renvoie plus de any
Données validées - Kanon s'assure que la réponse API correspond à votre schéma
Gestion d'erreurs élégante - Chaque erreur est capturée et typée
Transformations propres - les utilitaires Arkhe simplifient la mise en forme
Pipeline composable - Facile d'ajouter du cache, des retries ou du logging
Comparer au code traditionnel
Sans Pithos, cela impliquerait typiquement :
// ❌ L'approche traditionnelle
async function loadDashboard(userId: string) {
try {
const response = await fetch(`/api/dashboard/${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json(); // any 😱
// Validation manuelle...
if (!data.user || !data.posts) {
throw new Error("Données invalides");
}
// Transformation manuelle...
return {
user: {
fullName: data.user.firstName + " " + data.user.lastName,
// ... plus de travail manuel
},
// ... plus de travail manuel
};
} catch (error) {
console.error(error); // Et maintenant ?
return null; // L'appelant doit vérifier null partout
}
}
Prochaines étapes
Maintenant que vous avez vu comment les modules fonctionnent ensemble :
- Explorez les utilitaires Arkhe pour plus de manipulation de données
- Apprenez les schémas Kanon pour la validation complexe
- Maîtrisez les patterns Zygos pour la gestion avancée des erreurs
- Parcourez les cas d'usage pour des problèmes spécifiques que vous résolvez