Component / Composable / Server

Vue d'Ensemble de l'Architecture

L'application suit une architecture en couches qui sépare clairement les responsabilités entre la présentation, la logique métier, et la gestion des données. Cette séparation permet une meilleure maintenabilité, testabilité, et évolutivité du code.

graph TB
    subgraph "Client Browser"
        UI[Interface Utilisateur]
    end

    subgraph "Nuxt 3 Application"
        subgraph "Frontend Layer"
            Components[Vue Components]
            Composables[Composables Directory]
            Store[Pinia Stores]
        end

        subgraph "Server Layer"
            API[API Routes]
            SDK[Djust SDK]
        end
    end

    subgraph "External Services"
        Djust[Djust Back End]
        CMS[Storyblok CMS]
    end

    UI --> Components
    Components --> Composables
    Composables --> Store
    Composables --> API
    SDK --> Djust
    API --> SDK
    Composables --> CMS

Organisation des Dossiers Principaux

1. 📁 components/ - Couche de Présentation

Le dossier components contient tous les composants Vue réutilisables organisés selon la méthodologie Atomic Design. Ces composants sont responsables de l'affichage et de l'interaction utilisateur.

Structure Organisationnelle

components/
├── atoms/          # Composants de base (boutons, inputs, images)
├── molecules/      # Combinaisons d'atomes (formulaires, cartes)
├── organisms/      # Structures complexes (header, footer, listes)
├── pages/          # Composants spécifiques aux pages
├── templates/      # Layouts et structures de page
└── layouts/        # Layouts généraux (auth, default, blank)

Responsabilités

  • Affichage des données reçues via les props
  • Gestion des événements utilisateur (clicks, saisies, etc.)
  • Émission d'événements vers les composants parents
  • Appel des composables pour la logique métier
  • Gestion de l'état local du composant

Exemple d'Interaction

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>

<script setup lang="ts">
interface Props {
  product: Product;
}

const props = defineProps<Props>();

// Appel du composable pour la logique métier
const { addProductToCart } = useCart();

const addToCart = async () => {
  try {
    await addProductToCart(props.product.id, 1);
    // Composant notifie le succès
    emit("product-added", props.product);
  } catch (error) {
    // Gestion d'erreur locale
    console.error("Failed to add product:", error);
  }
};
</script>

2. 🧩 composables/ - Couche Logique Métier

Le dossier composables contient la logique métier réutilisable sous forme de fonctions composables Vue 3. Ces fonctions encapsulent les appels API, la gestion d'état, et les opérations complexes.

Structure Par Feature

composables/
├── useCart/           # Gestion du panier
│   ├── index.ts       # Export principal
│   ├── useCartApi.ts  # Appels API
│   ├── useCartGetters.ts  # Getters du store
│   └── useCartHelpers.ts  # Fonctions utilitaires
├── useDjustAuth/      # Authentification
├── useProduct/        # Gestion produits
└── useOrder/          # Gestion commandes

Responsabilités

  • Encapsulation de la logique métier spécifique aux features
  • Appels aux API backend via le dossier server/
  • Interaction avec les stores Pinia pour la gestion d'état
  • Gestion des erreurs et validation des données
  • Transformation des données entre API et composants

Flux de Données

sequenceDiagram
    participant C as Vue Component
    participant Comp as Composable
    participant Store as Pinia Store
    participant API as Server API
    participant DB as Database

    C->>Comp: Call composable function
    Comp->>Store: Get current state
    Store-->>Comp: Return state data
    Comp->>API: Make API request
    API->>DB: Database operation
    DB-->>API: Return data
    API-->>Comp: API response
    Comp->>Store: Update state
    Store-->>C: Reactive state update
    C-->>C: UI re-renders

Exemple de Composable

// composables/useCart/useCartApi.ts
export const useCartApi = () => {
  const cartStore = useCartStore();

  const addProductToCart = async (productId: string, quantity: number) => {
    try {
      // Appel vers le server/api
      const { data } = await useFetch("/api/carts/add-product", {
        method: "POST",
        body: { productId, quantity },
      });

      // Mise à jour du store
      cartStore.addProduct(data.value);

      return data.value;
    } catch (error) {
      console.error("Cart API error:", error);
      throw error;
    }
  };

  return { addProductToCart };
};

3. 🖥️ server/ - Couche Backend/API

Le dossier server contient la logique côté serveur qui s'exécute sur le serveur Nuxt. Cette couche gère les API routes et la communication avec le SDK.

Structure du Server

server/
├── api/               # Routes API (/api/*)
│   ├── auth/         # Authentification
│   ├── carts/        # Gestion paniers
│   ├── products/     # Gestion produits
│   └── orders/       # Gestion commandes
├── middleware/       # Middleware serveur
├── plugins/          # Plugins serveur (Sentry, etc.)
└── utils/           # Utilitaires serveur

Responsabilités

  • Exposition des API REST pour les composables
  • Authentification et autorisation des requêtes
  • Communication avec les services externes (APIs tierces)
  • Transformation et validation des données
  • Gestion des erreurs serveur et logging
  • Optimisation des performances (cache, mise en pool)

Exemple d'API Route

// server/api/carts/add-product.post.ts
export default defineEventHandler(async (event) => {
  try {
    // Validation de l'authentification
    const user = await ensureAuthenticated(event);

    // Lecture du body de la requête
    const { productId, quantity } = await readBody(event);

    // Validation des données
    if (!productId || !quantity) {
      throw createError({
        statusCode: 400,
        statusMessage: "Product ID and quantity are required",
      });
    }

    // Appel au service externe via SDK
    const cartService = new CartService();
    const result = await cartService.addProduct(user.id, productId, quantity);

    // Retour de la réponse
    return {
      success: true,
      data: result,
    };
  } catch (error) {
    // Gestion d'erreur centralisée
    throw createError({
      statusCode: error.statusCode || 500,
      statusMessage: error.message || "Internal server error",
    });
  }
});

Server-Side Rendering (SSR) avec Nuxt 3

Qu'est-ce que le SSR ?

Le Server-Side Rendering est une technique où les appels api sont faits côté serveur plutôt que côté client. Nuxt 3 fournit cette fonctionnalité automatiquement avec des optimisations avancées.

Flux de Données Complet

Exemple : Ajout d'un Produit au Panier

graph TD
    A[User clicks Add to Cart] --> B[Vue Component]
    B --> C[useCart Composable]
    C --> D[useFetch to /api/carts/add]
    D --> E[Server API Route]
    E --> F[Authentication Check]
    F --> G[Cart Service]
    G --> H[Database Update]
    H --> I[Return Response]
    I --> J[Update Pinia Store]
    J --> K[Reactive UI Update]

    style A fill:#ffeb3b
    style B fill:#e1f5fe
    style C fill:#e8f5e8
    style E fill:#f3e5f5
    style J fill:#fff3e0

Code Détaillé du Flux

1. Composant Vue (components/)

<template>
  <button @click="handleAddToCart" :disabled="pending">
    {{ pending ? "Adding..." : "Add to Cart" }}
  </button>
</template>

<script setup lang="ts">
const props = defineProps<{ productId: string }>();

// Utilisation du composable
const { addProductToCart, pending } = useCart();

const handleAddToCart = async () => {
  await addProductToCart(props.productId, 1);
  // L'UI se met à jour automatiquement via la réactivité
};
</script>

2. Composable (composables/)

// composables/useCart/useCartApi.ts
export const useCartApi = () => {
  const pending = ref(false);

  const addProductToCart = async (productId: string, quantity: number) => {
    pending.value = true;
    try {
      // Appel automatiquement optimisé pour SSR/Client
      const { data } = await useFetch("/api/carts/add-product", {
        method: "POST",
        body: { productId, quantity },
      });

      // Mise à jour du store global
      const cartStore = useCartStore();
      cartStore.addProduct(data.value);

      return data.value;
    } finally {
      pending.value = false;
    }
  };

  return { addProductToCart, pending };
};

3. API Route (server/)

// server/api/carts/add-product.post.ts
export default defineEventHandler(async (event) => {
  // Authentification automatique
  const user = await ensureAuthenticated(event);
  const { productId, quantity } = await readBody(event);

  // Logique métier
  const cartService = new CartService();
  const result = await cartService.addProduct(user.cartId, productId, quantity);

  return {
    success: true,
    data: result,
  };
});

Avantages de Cette Architecture

1. Séparation des Responsabilités

  • Components : Interface utilisateur pure
  • Composables : Logique métier réutilisable
  • Server : API et logique serveur

2. Réutilisabilité

  • Composables partagés entre plusieurs composants
  • Components modulaires et testables
  • API routes documentées et versionnées

3. Performance

  • SSR pour le chargement initial
  • Client-side routing pour la navigation
  • Cache intelligent à tous les niveaux

4. Maintenabilité

  • Code organisé par feature
  • Types TypeScript stricts
  • Gestion d'erreurs centralisée

5. Évolutivité

  • Architecture modulaire
  • Ajout facile de nouvelles features
  • Séparation claire frontend/backend

Bonnes Pratiques

Pour les Components

  • Garder les composants purs et prévisibles
  • Utiliser les composables pour la logique complexe
  • Émettre des événements plutôt que modifier les props

Pour les Composables

  • Une responsabilité par composable
  • Gestion d'erreurs robuste
  • Types TypeScript explicites

Pour les Server Routes

  • Validation systématique des inputs
  • Authentification/autorisation appropriée
  • Réponses d'erreur cohérentes

Pour le SSR

  • Éviter les API côté serveur uniquement dans les composables
  • Utiliser process.server/process.client quand nécessaire
  • Optimiser les requêtes pour éviter les waterfalls

Cette architecture garantit une application robuste, performante et maintenant, tout en tirant parti des avantages du SSR pour l'expérience utilisateur et le SEO.