Conventions de développement
Frontières de modules (serveur)
- Chaque feature appartient à un module métier sous
app/mypromo/server/modules/. - Importer depuis un autre module uniquement via
app/mypromo/server/modules/<module>/index.ts. - Règles de couche : règles métier dans
domain, orchestration dansapplication, adaptateurs externes dansinfrastructure, handlers de routes dansapi. - Les primitives partagées utilisées par plugins, middleware ou routes proviennent de
app/mypromo/server/modules/shared/index.ts— jamais depuis des sous-chemins internes tels queapp/mypromo/server/modules/shared/apiouapp/mypromo/server/modules/shared/infrastructure/*. - Les usages cross-module des notifications passent par
app/mypromo/server/modules/notifications/index.ts; les chemins internesemail/*restent privés au module. - Piège Nitro — pas de
export * from './x'dans un baril sousserver/modules/. Nitro scanneserver/modules/**et charge chaqueindex.tscomme module Nitro (_resolveNitroModule, via jiti). Un baril qui combine plusieursexport * from './x'(surtout unindex.tsracine de module qui ré-exporte./domain,./application,./infrastructureen étoile) peut faire planternuxt prepare/typecheckavecERROR Cannot redefine property: setup— unObject.definePropertynon configurable réappliqué deux fois lors de la résolution concurrente des modules Nitro. Utiliser des ré-exports nommés explicites (export { a, b, type C } from './x') dans tous les barilsdomain/index.ts,application/index.ts,infrastructure/index.tset dans leindex.tsracine d'un module. Voir issue #129 pour la régression introduite par un barilnotifications/domain/index.tsenexport *.
Frontières du shell frontend
- Ce dépôt héberge deux applications frontend distinctes :
app/mypromo(le produit applicatif — auth, onboarding, espaces de travail par rôle, admin, APIs) etapp/landing(la vitrine publique BMP destinée aux marques / industriels, magasins et agences — direction de septembre 2026, cf.app/landing/README.md). Ce sont deux apps Nuxt séparées, chacune avec son proprenuxt.config.ts, ses dépendances et son pipeline de build — pas un layout partagé. - La distinction shell applicatif / vitrine reste valable : elle se trace maintenant entre les deux répertoires plutôt qu'entre ce dépôt et un projet externe.
app/mypromo/app/pages/*ne doit toujours pas devenir des pages marketing (hero, preuve sociale, pages d'intention de recherche…), etapp/landingne doit pas absorber de logique applicative (auth, onboarding, espaces de travail par rôle). - Maintenir les pages applicatives sur
app/mypromo/app/layouts/default.vue(auth, onboarding, espaces de travail par rôle, admin). - Ne pas ajouter de nouvelles routes ou logiques de layout vitrine dans
app/mypromo— une nouvelle page vitrine va dansapp/landing, déclarée dansshared/canonical-routes.ts(voirapp/landing/README.md). - Les anciens chemins vitrine encore présents dans
app/mypromo, si nécessaire, restent des redirections de compatibilité vers/authuniquement — ce ne sont pas des pages vitrine à faire évoluer ; la vitrine active vit dansapp/landing. - Pour les pages applicatives volumineuses, extraire les contrôles UI pilotés par données en composants dédiés (ex. sélecteurs de filtres gérant leur propre chargement API/état) afin de garder les pages en mode orchestration uniquement.
Découpage des fichiers Vue
Principe général
- Les pages (
app/mypromo/app/pages/*) n'orchestrent que : paramètres de route, chargement async, état de haut niveau, câblage entre blocs fonctionnels. - Extraire un composant réutilisable dès qu'un bloc de template apparaît en deux endroits, ou quand une page dépasse environ 300-400 LOC avec des responsabilités mélangées.
- Découper par domaine métier (
store,organization,ad-space,campaign) avant tout. - Un composant a une responsabilité dominante : carte d'affichage, bloc de filtre, étape d'éditeur, panneau récapitulatif, etc.
- Déplacer la logique d'intégration lourde (cartes, polling API, upload, état synchronisé sur l'URL) dans des composables sous
app/mypromo/app/composables/*.
Checklist d'extraction d'un composant enfant
Extraire dès qu'au moins une condition est vraie :
- Balisage ou comportement répété.
- Le bloc a son propre état local et sa propre validation.
- Le bloc a plus d'un événement/sortie.
- La lisibilité du parent en pâtit.
Responsabilité du composant parent :
- Récupérer/sauvegarder les données.
- États globaux de chargement/erreur.
- Navigation et feedback de niveau page.
Responsabilité du composant enfant :
- Présentation locale.
- Normalisation locale des inputs.
- Émission d'événements typés.
Contrats de composants
- Définir des contrats typés explicites avec
definePropset des émissions typées. - Préférer les bindings
v-modelpour les contrôles réutilisables de type formulaire, avec des noms d'événements stables et des formes de valeurs prévisibles. - Éviter de passer des payloads backend bruts profondément dans l'arbre ; les mapper vers des view models adaptés à l'UI à la frontière de la feature.
- Garder les listes statiques d'options près du composant propriétaire (
options.ts) plutôt que dans des constantes de niveau page.
Niveaux de réutilisabilité
app/mypromo/app/components/<entity>/*: composants métier cross-feature (défaut recommandé pour les blocs réutilisables).app/mypromo/app/components/<feature>/*: blocs de composition locaux à une feature, pas encore partagés.app/mypromo/app/components/*(racine) : uniquement pour des blocs de construction largement réutilisables et agnostiques au domaine.
Nommage à l'usage (auto-import) : un composant placé dans un sous-dossier (app/components/<dossier>/Foo.vue) est auto-importé par Nuxt avec un nom préfixé par le chemin du dossier (ex. app/components/reservations/CounterProposalModal.vue → <ReservationsCounterProposalModal>, app/components/admin/promo-furnitures/CreateFurnitureCard.vue → <AdminPromoFurnituresCreateFurnitureCard>). Toujours référencer le composant dans le template avec ce nom préfixé, pas avec le nom de fichier nu — sinon Nuxt échoue à le résoudre à l'exécution (Failed to resolve component, souvent accompagné d'une discordance d'hydratation côté SSR).
Anti-patterns à éviter
- Fichiers de page volumineux mélangeant appels API, règles métier, règles de formulaire et rendu complet du template.
- Composants enfants produisant des effets de bord parents cachés (navigation directe, mutation du store global sans contrat).
- Variantes copier-coller du même bloc avec de légères différences de libellé (utiliser props/slots/options).
- "God composables" non scopés agrégeant des responsabilités sans rapport.
Composants UI
Utiliser Nuxt UI 4 comme choix par défaut pour les primitives frontend et les interactions dès qu'un équivalent existe (UInput, UTextarea, USelect, USelectMenu, UButton, UCard, UAlert, etc.). Ne garder les éléments HTML natifs que pour des cas justifiés (comportement de composant manquant, contraintes d'accessibilité, besoin d'intégration bas niveau).
Pour les collections en admin/serveur, standardiser le rendu des listes avec <AkDataList> (de @aidalinfo/nuxt-ui-kit) piloté par useServerAdminList (recherche, pagination, synchronisation URL) sauf si une tâche exige une visualisation différente. UTable + TableTemplate restent utilisés dans les workspaces non-admin et les vues héritées où ils sont légitimement en place.
Pour les valeurs de référence détaillées (layout, en-têtes de page, couleurs, typographie, tables, boutons, notifications), voir le Guide UI.
Internationalisation (i18n)
- Utiliser
@nuxtjs/i18ncomme source de vérité unique pour la localisation du texte applicatif. - Conserver les locales dans
app/mypromo/i18n/locales/*.jsonet préférer des clés stables et namespacées (layout.*,auth.*,onboarding.*, etc.). - Garder la stratégie de routing
no_prefixsauf décision produit explicite de localiser les URLs. - Dans
app/mypromo/app/app.vue, lier la locale deUAppàuseI18n().localeavec@nuxt/ui/localepour aligner les libellés intégrés de Nuxt UI et les formats date/heure avec la langue active. - Pour les composants réutilisables, fournir des valeurs par défaut compatibles i18n (libellés/placeholders traduits par défaut) tout en autorisant des surcharges explicites via props.
API et erreurs
- Utiliser les helpers partagés de
app/mypromo/server/modules/shared/apipour les handlers de routes. - Pour les routes auth protégées, utiliser les wrappers RBAC de
app/mypromo/server/modules/auth/api/middleware.ts. - Toujours retourner des enveloppes standardisées (
ok,data/error,meta). - Utiliser des codes d'erreur stables (
API_ERROR_CODES) et éviter les chaînes libres fragiles. - Pour tout appel à une route authentifiée exécuté pendant le bootstrap SSR d'une page (
awaitau niveau racine du<script setup>, y compris dans unuseAsyncData), utiliseruseRequestFetch()et non$fetchbrut :$fetchne propage pas le cookie de session entrant côté serveur, ce qui provoque un 401 à l'ouverture directe ou au rechargement de la page alors que la navigation SPA fonctionne.$fetchreste correct pour les appels déclenchés côté client par une interaction utilisateur (submit, clic).
Tracing des requêtes et logs
- Propager
requestIddu middleware vers tous les logs. - Émettre des logs JSON structurés avec
timestamp,level,scope,message,requestId. - Ne jamais logger de secrets (clés, mots de passe, tokens, credentials complets).
Sécurité
- Ne pas inclure de secrets d'application de repli dans les chemins de code (les secrets auth/session doivent être explicites dans l'environnement).
- Protéger les endpoints auth et admin critiques avec des contrôles anti-abus (rate limiting a minima).
- Préférer des flux de consentement utilisateur explicites pour les changements de membres d'organisation (invitation + acceptation, jamais d'attachement direct).
Géocodage
- Limiter l'usage de Google Maps aux workflows de géo-référencement (onboarding, import de magasins, rafraîchissement admin du géoréférencement).
- Ne jamais appeler le géocodage Google depuis les endpoints de recherche/affichage de carte ; utiliser le champ
stores.latitude/stores.longitudepersisté.
Stockage de fichiers
- Centraliser les contextes d'upload, les contraintes MIME/taille et les conventions de dossier dans des helpers partagés (
app/mypromo/server/modules/shared/domain/file-storage.ts,app/mypromo/server/modules/shared/application/file-storage.ts). - Éviter les chemins de stockage codés en dur par feature dans les handlers/services ; résoudre les chemins via les IDs de contexte et les segments de scope.
- Limiter le service de fichiers publics aux contextes explicites ; les contextes privés nécessitent des endpoints authentifiés dédiés.
- Contrat d'upload standard pour les APIs : accepter les items de payload
files[]sérialisés (name,contenten data URL base64,size,type,lastModified) générés depuis les sélectionsUFileUpload. - Orchestration serveur standard pour les services de features :
- Valider le payload domaine en premier.
- Persister les fichiers via
storeFilesForContextavec des segments de scope propres à la feature. - Stocker les URLs
/api/files/...résultantes dans les entités métier (jamais de chemins absolus locaux). - En mise à jour/suppression, supprimer les fichiers managés orphelins via
deleteStoredFilesByUrlsscopé par contexte + préfixe. - Assurer un rollback de nettoyage en cas d'échec partiel (l'écriture DB échoue après l'upload).
Portes de qualité
Validation locale avant PR :
pnpm run check # lint + typecheck + test:unit + test:integration
pnpm run build
pnpm run test:e2e # pour les flux UI/API
Le script check local de app/mypromo exécute lint + typecheck + test. Le script check à la racine du dépôt inclut en plus le vérificateur de format (format:check).
La règle de frontières de modules est vérifiée par app/mypromo/tests/unit/module-boundaries.spec.ts — doit rester verte.
La CI doit rester verte sur lint, typecheck, tests, build.
Commits et branches
- Suivre les Conventional Commits.
- Garder les commits focalisés par module/concern.
- Préférer des PRs petites et relisibles avec un scope explicite.