Vue d’ensemble
L’API Layer est une couche d’abstraction TypeScript qui centralise toutes les interactions avec le backend Nhost via GraphQL. Elle fournit des fonctions type-safe, une gestion d’erreurs cohérente et une détection automatique des erreurs réseau.
Principe fondamental :
Aucune requête GraphQL directe dans les composants. Tout passe par src/api/ via les wrappers apiCall() et apiCallVoid().
Avantages :
Type safety avec TypeScript strict
Gestion d’erreurs centralisée (ApiError)
Détection automatique des erreurs réseau
Logging via logger (jamais de console.log en prod)
Tests unitaires simplifiés
Refactoring facile (changement de backend transparent)
Structure :
src/api/
├── client.ts # Client Nhost singleton
├── base.ts # apiCall, apiCallVoid, ApiError
├── types.ts # Types API
├── auth.ts # Authentication (Nhost Auth)
├── tools.ts # Tools CRUD (GraphQL)
├── categories.ts # Categories CRUD
├── filters.ts # Filters CRUD
├── workflows.ts # Workflows CRUD
├── packs.ts # Tool Packs CRUD
├── quiz/ # Quiz operations
├── index.ts # Barrel exports
└── __tests__/ # Tests unitaires
Client Nhost
Configuration
Fichier : src/api/client.ts
import { createClient } from '@nhost/nhost-js' ;
const nhostSubdomain = import . meta . env . VITE_NHOST_SUBDOMAIN || 'gqvlmqwbsmkhlllmgbyw' ;
const nhostRegion = import . meta . env . VITE_NHOST_REGION || 'eu-central-1' ;
if ( ! nhostSubdomain || ! nhostRegion ) {
throw new Error ( 'Missing Nhost environment variables.' );
}
export const nhost = createClient ({
subdomain: nhostSubdomain ,
region: nhostRegion ,
});
Singleton : Une seule instance Nhost réutilisée partout. Le client expose automatiquement :
nhost.graphql — requêtes GraphQL vers Hasura
nhost.auth — authentification (signIn, signOut, session)
nhost.storage — upload/download de fichiers
Wrappers apiCall / apiCallVoid
apiCall — Pour les opérations qui retournent des données
Fichier : src/api/base.ts
export async function apiCall < T >(
operation : () => Promise <{ data : T ; error : unknown }>,
operationName : string
) : Promise < T > {
let result ;
try {
result = await operation ();
} catch ( error ) {
if ( isNetworkError ( error )) {
throw new ApiError ( 'NETWORK_UNAVAILABLE' );
}
throw new ApiError ( error instanceof Error ? error . message : 'Unknown error' );
}
const { data , error } = result ;
if ( error ) {
throw new ApiError ( /* message from error */ );
}
return data ;
}
apiCallVoid — Pour les opérations sans retour (mutations)
export async function apiCallVoid (
operation : () => Promise <{ error : unknown }>,
operationName : string
) : Promise < void > {
// Même logique, mais ne retourne rien
}
ApiError Class
export class ApiError extends Error {
constructor ( message : string , public code ?: string , public details ?: unknown ) {
super ( message );
this . name = 'ApiError' ;
}
}
Fichier : src/api/tools.ts
Les requêtes sont faites via nhost.graphql.request() avec des queries GraphQL :
import { apiCall , apiCallVoid } from './base' ;
import { nhost } from './client' ;
import type { EnhancedTool , ToolInsert } from './types' ;
// Helper interne : récupérer les catégories
async function fetchCategories () : Promise < Map < string , string >> {
const query = `query { categories { id name } }` ;
const result = await nhost . graphql . request ({ query });
if ( result . error ) return new Map ();
const categories = result . body ?. data ?. categories || [];
return new Map ( categories . map (( c : any ) => [ c . id , c . name ]));
}
export const toolsApi = {
/**
* Liste tous les outils avec catégorie et features enrichies
*/
async list () : Promise < EnhancedTool []> {
const query = `
query GetAllTools {
tools(order_by: { name: asc }) {
id name description pricing_tier category_id
logo_url website_url created_at
}
}
` ;
const data = await apiCall (
() => nhost . graphql . request ({ query }). then ( r => ({
data: r . body ?. data ?. tools || [],
error: r . error
})),
'tools.list'
);
// Enrichir avec noms de catégories et features
const categoryMap = await fetchCategories ();
return data . map ( tool => ({
... tool ,
category_name: categoryMap . get ( tool . category_id ) || 'Non catégorisé' ,
features: [] // Chargées séparément
}));
},
/**
* Créer un nouvel outil
*/
async create ( tool : ToolInsert ) : Promise < any > {
const mutation = `
mutation InsertTool($object: tools_insert_input!) {
insert_tools_one(object: $object) {
id name
}
}
` ;
return apiCall (
() => nhost . graphql . request ({
query: mutation ,
variables: { object: tool }
}). then ( r => ({
data: r . body ?. data ?. insert_tools_one ,
error: r . error
})),
'tools.create'
);
},
/**
* Mettre à jour un outil (convention Hasura : _set)
*/
async update ( id : string , updates : Partial < ToolInsert >) : Promise < any > {
const mutation = `
mutation UpdateTool($id: uuid!, $set: tools_set_input!) {
update_tools_by_pk(pk_columns: { id: $id }, _set: $set) {
id name
}
}
` ;
return apiCall (
() => nhost . graphql . request ({
query: mutation ,
variables: { id , set: updates }
}). then ( r => ({
data: r . body ?. data ?. update_tools_by_pk ,
error: r . error
})),
'tools.update'
);
},
/**
* Supprimer un outil
*/
async delete ( id : string ) : Promise < void > {
const mutation = `
mutation DeleteTool($id: uuid!) {
delete_tools_by_pk(id: $id) { id }
}
` ;
await apiCallVoid (
() => nhost . graphql . request ({
query: mutation ,
variables: { id }
}). then ( r => ({ error: r . error })),
'tools.delete'
);
}
};
Convention Hasura : Pour les mutations update, utilisez _set (avec underscore) pour définir les champs à mettre à jour.
Utilisation dans les composants
Pattern recommandé
❌ Mauvais : Requête GraphQL directe
function ToolsList () {
useEffect (() => {
// ❌ NE PAS FAIRE
nhost . graphql . request ({ query: '{ tools { id name } }' })
. then ( r => setTools ( r . body . data . tools ));
}, []);
}
✅ Bon : Via API Layer
import { toolsApi } from '@/api' ;
function ToolsList () {
const [ tools , setTools ] = useState < EnhancedTool []>([]);
useEffect (() => {
toolsApi . list ()
. then ( setTools )
. catch ( err => {
if ( err instanceof ApiError ) {
logger . error ( err . message );
}
});
}, []);
}
Avec Custom Hook (meilleure approche)
import { useAppData } from '@/hooks/useAppData' ;
function ToolsList () {
const { tools , isLoading , error } = useAppData ();
if ( isLoading ) return < LoadingSpinner />;
if ( error ) return < ErrorMessage message ={ error } />;
return (
< div className = "grid md:grid-cols-3 gap-4" >
{ tools . map ( tool => (
< ToolCard key = {tool. id } tool = { tool } />
))}
</ div >
);
}
Bonnes pratiques
✅ À faire
Toujours utiliser apiCall/apiCallVoid
// ✅ Bon
const data = await apiCall (
() => nhost . graphql . request ({ query , variables }),
'operationName'
);
// ❌ Mauvais
const result = await nhost . graphql . request ({ query });
Utiliser logger au lieu de console.log
// ✅ Bon
logger . error ( 'API error:' , error , { component: 'api' });
// ❌ Mauvais (pas autorisé en production)
console . log ( 'error' , error );
Typer les retours API
async list (): Promise < EnhancedTool [] > { }
❌ À éviter
Ne pas appeler nhost.graphql directement dans les composants. Toujours passer par src/api/.
Ne pas ignorer les erreurs silencieusement.
Ne pas utiliser de types any — préférer les types définis dans src/api/types.ts.
Ressources
Nhost & GraphQL Architecture backend complète
Database Schema Structure des tables pour comprendre les relations
Custom Hooks Hooks qui utilisent l’API Layer
Testing Guide complet pour tester l’API