Skip to main content

Principe fondamental

Règle d’or : Aucun composant ne doit appeler nhost.graphql directement. Toutes les interactions passent par le dossier src/api/ via les wrappers apiCall() et apiCallVoid().

❌ Ne jamais faire

// Dans un composant React
import { nhost } from '@/api/client';

function ToolsList() {
  useEffect(() => {
    // ❌ Appel GraphQL direct dans le composant
    nhost.graphql.request({ query: '{ tools { id name } }' })
      .then(r => setTools(r.body.data.tools));
  }, []);
}

✅ Toujours faire

// Via l'API layer
import { toolsApi } from '@/api';

function ToolsList() {
  useEffect(() => {
    toolsApi.list().then(setTools);
  }, []);
}

Structure de l’API layer

src/api/
├── base.ts                 # apiCall, apiCallVoid, ApiError
├── client.ts               # Client Nhost singleton
├── types.ts                # Types partagés
├── index.ts                # Barrel exports

├── auth.ts                 # Authentification (Nhost Auth)
├── tools.ts                # CRUD Tools (GraphQL)
├── categories.ts           # CRUD Categories
├── filters.ts              # CRUD Filters/Features
├── workflows.ts            # CRUD Workflows
├── packs.ts                # CRUD Tool Packs
├── quiz/                   # CRUD Quiz (multi-fichiers)

└── __tests__/              # Tests API
    ├── tools.spec.ts
    └── ...

Fichiers de base

client.ts - Client Nhost singleton

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';

export const nhost = createClient({
  subdomain: nhostSubdomain,
  region: nhostRegion,
});
Pourquoi un singleton ?
  • Une seule instance partagée
  • Gestion de session centralisée par Nhost
  • Token JWT automatiquement injecté dans les requêtes

base.ts - Wrappers et gestion d’erreurs

export class ApiError extends Error {
  constructor(message: string, public code?: string, public details?: unknown) {
    super(message);
    this.name = 'ApiError';
  }
}

// Pour les opérations qui retournent des données
export async function apiCall<T>(
  operation: () => Promise<{ data: T; error: unknown }>,
  operationName: string
): Promise<T> { /* ... */ }

// Pour les mutations sans retour
export async function apiCallVoid(
  operation: () => Promise<{ error: unknown }>,
  operationName: string
): Promise<void> { /* ... */ }
Bénéfices :
  • Erreurs typées (ApiError) et prévisibles
  • Logging centralisé via logger (pas de console.log)
  • Détection automatique des erreurs réseau
  • Nom d’opération pour le debugging

Patterns d’utilisation

Pattern 1 : Query simple

import { apiCall } from './base';
import { nhost } from './client';

async function listTools() {
  const query = `
    query GetTools {
      tools(order_by: { name: asc }) {
        id name description pricing_tier
      }
    }
  `;

  return apiCall(
    () => nhost.graphql.request({ query }).then(r => ({
      data: r.body?.data?.tools || [],
      error: r.error
    })),
    'tools.list'
  );
}

Pattern 2 : Mutation avec variables

async function createTool(tool: ToolInsert) {
  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'
  );
}

Pattern 3 : Update avec _set (convention Hasura)

async function updateTool(id: string, updates: Partial<ToolInsert>) {
  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'
  );
}
Convention Hasura : Utilisez _set (avec underscore) pour les champs à mettre à jour dans les mutations. C’est le format attendu par Hasura.

Pattern 4 : Delete (void)

async function deleteTool(id: string) {
  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'
  );
}

Authentification

auth.ts

import { nhost } from './client';

export const authApi = {
  async signIn(email: string, password: string) {
    const result = await nhost.auth.signIn({ email, password });
    if (result.error) throw new ApiError(result.error.message);
    return result.session;
  },

  async signOut() {
    const result = await nhost.auth.signOut();
    if (result.error) throw new ApiError(result.error.message);
  },

  getSession() {
    return nhost.auth.getSession();
  },

  isAuthenticated() {
    return !!nhost.auth.getSession();
  }
};

Bonnes pratiques

✅ À faire

  1. Toujours utiliser apiCall/apiCallVoid
  2. Toujours typer les retours : Promise<EnhancedTool[]>
  3. Utiliser logger au lieu de console.log
  4. Nommer les opérations : 'tools.list', 'workflows.create'
  5. Valider les entrées côté client avant d’envoyer

❌ À éviter

  1. Appels nhost.graphql directs dans les composants
  2. Ignorer les erreurs silencieusement
  3. Types any — utiliser src/api/types.ts
  4. console.log en production — utiliser logger

Ressources

Nhost & GraphQL

Architecture backend complète

Database Schema

Structure des 10 tables

Hooks

Utiliser l’API dans des hooks

Testing

Tester l’API layer