# Décisions Architecturales - Xpeditis **Projet**: Xpeditis - Plateforme B2B SaaS maritime **Format**: Architecture Decision Records (ADR) **Dernière mise à jour**: 2025-12-22 --- ## Index des décisions | ID | Date | Titre | Status | |----|------|-------|--------| | ADR-001 | 2025-12-22 | Adoption de l'architecture hexagonale | ✅ Acceptée | | ADR-002 | 2025-12-22 | Suppression dépendances NestJS du domain layer | 🟡 Proposée | | ADR-003 | 2025-12-22 | Activation TypeScript strict mode frontend | 🟡 Proposée | | ADR-004 | 2025-12-22 | Standardisation pattern data fetching (React Query) | 🟡 Proposée | | ADR-005 | 2025-12-22 | Migration pagination client-side vers serveur | 🟡 Proposée | **Légende des status**: - ✅ Acceptée: Décision implémentée - 🟡 Proposée: En attente d'implémentation - ❌ Rejetée: Décision écartée - 🔄 Superseded: Remplacée par une autre décision --- ## ADR-001: Adoption de l'architecture hexagonale **Date**: 2025-12-22 (rétroactif - décision initiale du projet) **Status**: ✅ Acceptée et implémentée ### Contexte L'application Xpeditis nécessite: - Une séparation claire entre la logique métier et les détails techniques - La capacité de changer de framework sans réécrire le métier - Une testabilité maximale du code domaine - L'indépendance vis-à-vis des bases de données et APIs externes ### Décision Adopter l'**architecture hexagonale (Ports & Adapters)** pour le backend NestJS avec: **3 couches strictement séparées**: 1. **Domain**: Logique métier pure (zéro dépendance externe) 2. **Application**: Orchestration et points d'entrée (controllers, DTOs) 3. **Infrastructure**: Adapters externes (DB, cache, email, APIs) **Règles de dépendance**: - Infrastructure → Application → Domain - Jamais l'inverse - Les interfaces (ports) sont définies dans le domain - Les implémentations (adapters) sont dans l'infrastructure ### Conséquences **Positives**: - ✅ Domaine métier testable sans framework - ✅ Changement de DB/ORM sans impact sur le métier - ✅ Code domaine réutilisable - ✅ Séparation claire des responsabilités - ✅ Facilite l'onboarding des nouveaux développeurs **Négatives**: - ⚠️ Plus de fichiers à créer (ports, adapters, mappers) - ⚠️ Courbe d'apprentissage initiale - ⚠️ Verbosité accrue (mappers Domain ↔ ORM, Domain ↔ DTO) **Risques**: - ⚠️ Tentation de violer les règles (imports directs, shortcuts) - ⚠️ Over-engineering pour des features simples ### Implémentation **Structure adoptée**: ``` apps/backend/src/ ├── domain/ # 🔵 Cœur métier (aucune dépendance) │ ├── entities/ │ ├── value-objects/ │ ├── services/ │ ├── ports/ │ └── exceptions/ ├── application/ # 🔌 Controllers & Use Cases │ ├── controllers/ │ ├── dto/ │ ├── services/ │ └── mappers/ └── infrastructure/ # 🏗️ Adapters externes ├── persistence/ ├── cache/ ├── email/ └── carriers/ ``` **Validation**: - Tous les modules respectent la structure - 95% de conformité (1 violation identifiée - voir ADR-002) - Pattern Repository implémenté avec 13 repositories ### Alternatives considérées **1. Clean Architecture (Uncle Bob)** - Rejetée: Trop de couches (4-5) pour notre complexité - Architecture hexagonale est plus simple et suffisante **2. MVC traditionnel** - Rejetée: Pas de séparation domaine/infrastructure - Logique métier mélangée avec framework **3. Feature Modules seuls (NestJS standard)** - Rejetée: Domaine couplé à NestJS - Difficile à tester et réutiliser ### Références - [Hexagonal Architecture - Alistair Cockburn](https://alistair.cockburn.us/hexagonal-architecture/) - [docs/architecture.md](./architecture.md) - [CLAUDE.md](../CLAUDE.md) --- ## ADR-002: Suppression dépendances NestJS du domain layer **Date**: 2025-12-22 **Status**: 🟡 Proposée ### Contexte **Violation identifiée** lors de l'audit d'architecture: - Le fichier `domain/services/booking.service.ts` importe `@nestjs/common` - Utilise les decorators `@Injectable()` et `@Inject()` - Utilise `NotFoundException` (exception NestJS, pas métier) **Impact actuel**: - Couplage du domain layer avec le framework NestJS - Impossible de tester le service sans `TestingModule` - Violation du principe d'inversion de dépendance ### Décision **Supprimer toutes les dépendances NestJS du domain layer**: 1. Retirer `@Injectable()`, `@Inject()` de `BookingService` 2. Créer exception domaine `RateQuoteNotFoundException` 3. Adapter l'injection dans `bookings.module.ts` 4. Simplifier les tests unitaires ### Implémentation proposée **Étape 1**: Refactoring du service domaine ```typescript // domain/services/booking.service.ts // AVANT import { Injectable, Inject, NotFoundException } from '@nestjs/common'; @Injectable() export class BookingService { constructor( @Inject(BOOKING_REPOSITORY) private readonly bookingRepository: BookingRepository, ) {} } // APRÈS import { RateQuoteNotFoundException } from '../exceptions'; export class BookingService { constructor( private readonly bookingRepository: BookingRepository, private readonly rateQuoteRepository: RateQuoteRepository, ) {} } ``` **Étape 2**: Nouvelle exception domaine ```typescript // domain/exceptions/rate-quote-not-found.exception.ts export class RateQuoteNotFoundException extends Error { constructor(public readonly rateQuoteId: string) { super(`Rate quote with id ${rateQuoteId} not found`); this.name = 'RateQuoteNotFoundException'; } } ``` **Étape 3**: Adapter le module application ```typescript // application/bookings/bookings.module.ts @Module({ providers: [ { provide: BookingService, useFactory: (bookingRepo, rateQuoteRepo) => { return new BookingService(bookingRepo, rateQuoteRepo); }, inject: [BOOKING_REPOSITORY, RATE_QUOTE_REPOSITORY], }, ], }) ``` ### Conséquences **Positives**: - ✅ Conformité 100% architecture hexagonale - ✅ Domain layer totalement indépendant du framework - ✅ Tests plus simples et plus rapides - ✅ Réutilisabilité du code domaine **Négatives**: - ⚠️ Légère verbosité dans la configuration des modules - ⚠️ Besoin de mapper les exceptions (domaine → HTTP) dans application layer **Risques**: - ⚠️ **FAIBLE**: Risque de régression (tests doivent passer) - ⚠️ **FAIBLE**: Impact sur les features dépendantes de BookingService ### Validation **Critères d'acceptation**: - [ ] ✅ Aucun import `@nestjs/*` dans `domain/` - [ ] ✅ Tous les tests unitaires passent - [ ] ✅ Tests d'intégration passent - [ ] ✅ Tests E2E passent - [ ] ✅ Pas de régression fonctionnelle **Commande de vérification**: ```bash # Vérifier l'absence d'imports NestJS dans domain grep -r "from '@nestjs" apps/backend/src/domain/ # Résultat attendu: Aucun résultat ``` ### Timeline **Estimation**: 2-3 heures - Refactoring: 1h - Tests: 1h - Review: 30min ### Références - [docs/backend/cleanup-report.md](./backend/cleanup-report.md) - [Hexagonal Architecture Principles](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/) --- ## ADR-003: Activation TypeScript strict mode frontend **Date**: 2025-12-22 **Status**: 🟡 Proposée ### Contexte **Problème identifié**: - `tsconfig.json` a `"strict": false` - Permet les erreurs de type silencieuses - Risque de bugs runtime (`undefined is not a function`, `Cannot read property of null`) - Code non type-safe difficile à maintenir **Exemples de bugs non détectés sans strict mode**: ```typescript // ❌ Accepté sans strict mode let user: User; console.log(user.name); // user peut être undefined function getBooking(id?: string) { return bookings.find(b => b.id === id); // id peut être undefined } ``` ### Décision **Activer TypeScript strict mode** dans `tsconfig.json`: ```json { "compilerOptions": { "strict": true } } ``` Ou activer les flags individuellement: ```json { "compilerOptions": { "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitAny": true, "noImplicitThis": true, "alwaysStrict": true } } ``` ### Conséquences **Positives**: - ✅ Détection des bugs au build time (pas runtime) - ✅ Meilleure autocomplétion IDE (IntelliSense) - ✅ Refactoring plus sûr - ✅ Documentation implicite via les types - ✅ Réduction des erreurs en production **Négatives**: - ⚠️ Corrections TypeScript nécessaires (estimation: 50-70% des fichiers) - ⚠️ Apprentissage des patterns de null safety - ⚠️ Code plus verbeux (guards, optional chaining) **Risques**: - ⚠️ **FAIBLE**: Pas de risque fonctionnel (corrections statiques) - ⚠️ **MOYEN**: Temps de correction estimé à 2-3 jours ### Implémentation **Phase 1: Activation progressive** ```bash # 1. Activer strict mode # tsconfig.json: "strict": true # 2. Lister toutes les erreurs npm run type-check 2>&1 | tee typescript-errors.log # 3. Trier par fichier/type d'erreur cat typescript-errors.log | sort | uniq -c ``` **Phase 2: Patterns de correction** **Pattern 1**: Null checks ```typescript // AVANT (strict: false) function BookingDetails({ booking }) { return
{booking.customerName}
; } // APRÈS (strict: true) interface BookingDetailsProps { booking: Booking | null; } function BookingDetails({ booking }: BookingDetailsProps) { if (!booking) return
Loading...
; return
{booking.customerName}
; } ``` **Pattern 2**: Optional chaining ```typescript // AVANT const name = user.organization.name; // APRÈS const name = user?.organization?.name; ``` **Pattern 3**: Nullish coalescing ```typescript // AVANT const limit = params.limit || 20; // APRÈS const limit = params.limit ?? 20; // Gère correctement 0 ``` **Phase 3: Validation** ```bash # Vérifier qu'il n'y a plus d'erreurs TypeScript npm run type-check # Résultat attendu: Found 0 errors # Build production npm run build # Résultat attendu: Build successful ``` ### Timeline **Estimation**: 2-3 jours - Jour 1: Activer strict mode + lister erreurs - Jour 2: Corriger 70% des erreurs - Jour 3: Corriger les 30% restants + validation ### Alternatives considérées **1. Garder strict: false** - Rejetée: Accumulation de dette technique - Risque de bugs en production **2. Activation progressive flag par flag** - Possible mais plus long - Préférer activation directe (plus rapide) **3. Migration vers Zod/io-ts pour runtime validation** - Complémentaire (pas alternative) - Peut être ajouté après strict mode ### Références - [TypeScript Strict Mode Guide](https://www.typescriptlang.org/tsconfig#strict) - [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md) --- ## ADR-004: Standardisation pattern data fetching (React Query) **Date**: 2025-12-22 **Status**: 🟡 Proposée ### Contexte **Problème**: 3 patterns différents utilisés dans le frontend **Pattern 1**: React Query + API client (✅ Recommandé) ```typescript const { data } = useQuery({ queryKey: ['dashboard', 'kpis'], queryFn: () => getDashboardKpis(), }); ``` **Pattern 2**: Custom hook avec fetch direct (❌ Problématique) ```typescript const response = await fetch(`/api/v1/bookings/search`, { headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` }, }); ``` **Pattern 3**: API client direct dans composant (⚠️ Acceptable) ```typescript const { data } = useQuery({ queryKey: ['bookings'], queryFn: () => listBookings({ page: 1, limit: 20 }), }); ``` **Problèmes identifiés**: - Incohérence dans le codebase - Token management en doublon - Error handling différent partout - Pas de cache centralisé - Retry logic manquante ### Décision **Standardiser sur Pattern 1**: **React Query + API client partout** **Raisons**: 1. Token management centralisé (dans apiClient) 2. Error handling uniforme 3. Cache management automatique 4. Retry logic configurée 5. Type safety maximale 6. Optimistic updates possibles ### Implémentation **Refactoring type**: **AVANT** (useBookings.ts - Pattern 2): ```typescript export function useBookings() { const [bookings, setBookings] = useState([]); const [loading, setLoading] = useState(false); const searchBookings = async (filters) => { setLoading(true); const response = await fetch(`/api/v1/bookings/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('accessToken')}`, }, body: JSON.stringify(filters), }); const data = await response.json(); setBookings(data); setLoading(false); }; return { bookings, loading, searchBookings }; } ``` **APRÈS** (useBookings.ts - Pattern 1): ```typescript import { useQuery } from '@tanstack/react-query'; import { advancedSearchBookings } from '@/lib/api/bookings'; export function useBookings(filters: BookingFilters) { return useQuery({ queryKey: ['bookings', 'search', filters], queryFn: () => advancedSearchBookings(filters), enabled: !!filters, staleTime: 5 * 60 * 1000, // 5 minutes }); } // Usage dans composant const { data, isLoading, error } = useBookings(filters); ``` **Configuration centralisée**: ```typescript // lib/providers/query-provider.tsx export function QueryProvider({ children }) { const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 minute retry: 3, refetchOnWindowFocus: false, }, mutations: { retry: 1, }, }, }); return ( {children} ); } ``` ### Conséquences **Positives**: - ✅ Code plus maintenable - ✅ Moins de bugs (error handling centralisé) - ✅ Meilleures performances (cache) - ✅ Meilleure UX (loading states, retry) - ✅ Dev experience améliorée (React Query DevTools) **Négatives**: - ⚠️ Refactoring nécessaire (estimation: 5-6 fichiers) - ⚠️ Dépendance à React Query (mais déjà présent) ### Timeline **Estimation**: 1 jour - Refactoring hooks: 4h - Tests: 2h - Documentation: 1h ### Fichiers à modifier 1. `src/hooks/useBookings.ts` (supprimer fetch direct) 2. `src/hooks/useCsvRateSearch.ts` (vérifier pattern) 3. `src/hooks/useNotifications.ts` (vérifier pattern) 4. Tout autre hook custom utilisant fetch direct ### Validation **Checklist**: - [ ] ✅ Aucun `fetch()` direct dans hooks/composants - [ ] ✅ Tous les calls API passent par `@/lib/api/*` - [ ] ✅ Tous les hooks utilisent `useQuery` ou `useMutation` - [ ] ✅ Token management unifié (via apiClient) **Commande de vérification**: ```bash # Chercher les fetch directs grep -r "fetch(" apps/frontend/src/ apps/frontend/app/ | grep -v "api/client.ts" # Résultat attendu: Aucun résultat (sauf dans client.ts) ``` ### Références - [React Query Best Practices](https://tkdodo.eu/blog/practical-react-query) - [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md) --- ## ADR-005: Migration pagination client-side vers serveur **Date**: 2025-12-22 **Status**: 🟡 Proposée ### Contexte **Problème actuel**: ```typescript // app/dashboard/bookings/page.tsx (ligne 29) listCsvBookings({ page: 1, limit: 1000 }) // ❌ Charge 1000 bookings ! // Puis pagination client-side const currentBookings = filteredBookings.slice(startIndex, endIndex); ``` **Impact**: - ⚠️ Transfert ~500KB-1MB de données - ⚠️ Temps de chargement initial: 2-3 secondes - ⚠️ Non scalable (impossible avec 10,000+ bookings) - ⚠️ UX dégradée (loading long) ### Décision **Implémenter pagination côté serveur** avec: 1. Requêtes paginées (20 items par page) 2. Filtres appliqués côté serveur 3. Cache React Query pour navigation rapide 4. Smooth transitions avec `keepPreviousData` ### Implémentation **AVANT**: ```typescript const { data: csvBookings } = useQuery({ queryKey: ['csv-bookings'], queryFn: () => listCsvBookings({ page: 1, limit: 1000 }), // ❌ Tout charger }); // Filtrage client-side const filteredBookings = bookings.filter(/* ... */); // Pagination client-side const currentBookings = filteredBookings.slice(startIndex, endIndex); ``` **APRÈS**: ```typescript const { data: csvBookings } = useQuery({ queryKey: ['csv-bookings', currentPage, filters], queryFn: () => listCsvBookings({ page: currentPage, limit: 20, // ✅ Pagination serveur ...filters, // ✅ Filtres serveur }), keepPreviousData: true, // ✅ Smooth transition entre pages }); // Plus besoin de pagination client-side const currentBookings = csvBookings?.data || []; const totalPages = csvBookings?.meta.totalPages || 1; ``` **Vérifier API backend**: ```typescript // Backend: apps/backend/src/application/controllers/csv-bookings.controller.ts @Get() async listCsvBookings( @Query('page') page: number = 1, @Query('limit') limit: number = 20, @Query() filters: CsvBookingFiltersDto, ): Promise> { // ✅ L'API supporte déjà la pagination return this.csvBookingService.findAll({ page, limit, filters }); } ``` ### Conséquences **Positives**: - ✅ Temps de chargement: 2s → 300ms - ✅ Taille transfert: 500KB → 20KB - ✅ Scalable: Supporte millions de records - ✅ Meilleure UX: Chargement instantané - ✅ Cache efficace: Une page = une requête **Négatives**: - ⚠️ Navigation entre pages = requête réseau - Mitigé par `keepPreviousData` (pas de flash de loading) - Mitigé par cache React Query (navigation arrière instantanée) **Risques**: - ⚠️ **FAIBLE**: Backend doit supporter les filtres serveur ### Validation **Critères de performance**: - [ ] ✅ Temps de chargement initial < 500ms - [ ] ✅ Navigation entre pages < 300ms - [ ] ✅ Taille transfert < 50KB par page - [ ] ✅ Pas de flash de loading (keepPreviousData) **Tests**: ```bash # Test avec 10,000 bookings en base # Avant: ~3s loading # Après: ~300ms loading ``` ### Timeline **Estimation**: 2-3 heures - Modification frontend: 1h - Vérification backend: 30min - Tests: 1h ### Fichiers à modifier 1. `app/dashboard/bookings/page.tsx` (refactoring pagination) 2. `lib/api/csv-bookings.ts` (vérifier support filtres serveur) 3. Potentiellement: `hooks/useBookings.ts` (si utilisé ailleurs) ### Références - [React Query Pagination Guide](https://tanstack.com/query/latest/docs/react/guides/paginated-queries) - [docs/frontend/cleanup-report.md](./frontend/cleanup-report.md) --- ## Template pour nouvelles décisions ```markdown ## ADR-XXX: [Titre de la décision] **Date**: YYYY-MM-DD **Status**: 🟡 Proposée / ✅ Acceptée / ❌ Rejetée / 🔄 Superseded ### Contexte [Décrire le problème ou la situation qui nécessite une décision] ### Décision [Décrire la décision prise et pourquoi] ### Implémentation [Décrire comment la décision sera implémentée] ### Conséquences **Positives**: - [Liste des avantages] **Négatives**: - [Liste des inconvénients] **Risques**: - [Liste des risques] ### Alternatives considérées [Décrire les autres options envisagées et pourquoi elles ont été rejetées] ### Validation [Décrire comment valider que la décision est correctement implémentée] ### Timeline [Estimation du temps nécessaire] ### Références [Liens vers documentation, articles, ADRs liés] ``` --- **Fin du document** Pour toute question ou proposition de nouvelle décision, contacter l'équipe architecture.