From 9b7af9f1b04eb6412ae9a86a70fbb056f04d29a2 Mon Sep 17 00:00:00 2001 From: MaksTinyWorkshop Date: Wed, 25 Mar 2026 08:32:13 +0100 Subject: [PATCH] Refonte Structure --- 00_INDEX.md | 32 +- 10_backend_patterns_valides.md | 1435 ----------------- 10_backend_risques_et_vigilance.md | 1145 ------------- 10_frontend_patterns_valides.md | 830 ---------- 10_frontend_risques_et_vigilance.md | 1127 ------------- 10_ux_patterns_valides.md | 70 - 10_ux_risques_et_vigilance.md | 49 - 95_a_capitaliser.md | 26 +- CLAUDE.md | 76 +- _AI_INSTRUCTIONS.md | 91 +- knowledge/backend/patterns/README.md | 18 + knowledge/backend/patterns/async.md | 79 + knowledge/backend/patterns/auth.md | 230 +++ knowledge/backend/patterns/contracts.md | 138 ++ knowledge/backend/patterns/multi-tenant.md | 188 +++ knowledge/backend/patterns/nestjs.md | 138 ++ knowledge/backend/patterns/nextjs.md | 166 ++ knowledge/backend/patterns/prisma.md | 247 +++ knowledge/backend/patterns/stripe.md | 160 ++ knowledge/backend/risques/README.md | 18 + knowledge/backend/risques/auth.md | 225 +++ knowledge/backend/risques/contracts.md | 165 ++ knowledge/backend/risques/general.md | 72 + knowledge/backend/risques/nestjs.md | 102 ++ knowledge/backend/risques/nextjs.md | 75 + knowledge/backend/risques/prisma.md | 306 ++++ knowledge/backend/risques/redis.md | 105 ++ knowledge/backend/risques/stripe.md | 116 ++ knowledge/frontend/patterns/README.md | 16 + knowledge/frontend/patterns/design-tokens.md | 186 +++ knowledge/frontend/patterns/forms.md | 130 ++ knowledge/frontend/patterns/navigation.md | 168 ++ knowledge/frontend/patterns/nextjs.md | 67 + knowledge/frontend/patterns/state.md | 189 +++ knowledge/frontend/patterns/tests.md | 53 + knowledge/frontend/risques/README.md | 18 + knowledge/frontend/risques/auth.md | 114 ++ knowledge/frontend/risques/design-tokens.md | 100 ++ knowledge/frontend/risques/general.md | 63 + knowledge/frontend/risques/navigation.md | 83 + knowledge/frontend/risques/nextjs.md | 378 +++++ knowledge/frontend/risques/performance.md | 57 + knowledge/frontend/risques/state.md | 280 ++++ knowledge/frontend/risques/tests.md | 46 + knowledge/n8n/patterns/README.md | 11 + .../n8n/patterns/general.md | 41 +- knowledge/n8n/risques/README.md | 11 + .../n8n/risques/general.md | 40 +- knowledge/product/patterns/README.md | 11 + .../product/patterns/general.md | 63 +- knowledge/product/risques/README.md | 12 + knowledge/ux/patterns/README.md | 12 + knowledge/ux/risques/README.md | 12 + knowledge/workflow/risques/README.md | 9 + knowledge/workflow/risques/story-tracking.md | 50 + 55 files changed, 4743 insertions(+), 4906 deletions(-) delete mode 100644 10_backend_patterns_valides.md delete mode 100644 10_backend_risques_et_vigilance.md delete mode 100644 10_frontend_patterns_valides.md delete mode 100644 10_frontend_risques_et_vigilance.md delete mode 100644 10_ux_patterns_valides.md delete mode 100644 10_ux_risques_et_vigilance.md create mode 100644 knowledge/backend/patterns/README.md create mode 100644 knowledge/backend/patterns/async.md create mode 100644 knowledge/backend/patterns/auth.md create mode 100644 knowledge/backend/patterns/contracts.md create mode 100644 knowledge/backend/patterns/multi-tenant.md create mode 100644 knowledge/backend/patterns/nestjs.md create mode 100644 knowledge/backend/patterns/nextjs.md create mode 100644 knowledge/backend/patterns/prisma.md create mode 100644 knowledge/backend/patterns/stripe.md create mode 100644 knowledge/backend/risques/README.md create mode 100644 knowledge/backend/risques/auth.md create mode 100644 knowledge/backend/risques/contracts.md create mode 100644 knowledge/backend/risques/general.md create mode 100644 knowledge/backend/risques/nestjs.md create mode 100644 knowledge/backend/risques/nextjs.md create mode 100644 knowledge/backend/risques/prisma.md create mode 100644 knowledge/backend/risques/redis.md create mode 100644 knowledge/backend/risques/stripe.md create mode 100644 knowledge/frontend/patterns/README.md create mode 100644 knowledge/frontend/patterns/design-tokens.md create mode 100644 knowledge/frontend/patterns/forms.md create mode 100644 knowledge/frontend/patterns/navigation.md create mode 100644 knowledge/frontend/patterns/nextjs.md create mode 100644 knowledge/frontend/patterns/state.md create mode 100644 knowledge/frontend/patterns/tests.md create mode 100644 knowledge/frontend/risques/README.md create mode 100644 knowledge/frontend/risques/auth.md create mode 100644 knowledge/frontend/risques/design-tokens.md create mode 100644 knowledge/frontend/risques/general.md create mode 100644 knowledge/frontend/risques/navigation.md create mode 100644 knowledge/frontend/risques/nextjs.md create mode 100644 knowledge/frontend/risques/performance.md create mode 100644 knowledge/frontend/risques/state.md create mode 100644 knowledge/frontend/risques/tests.md create mode 100644 knowledge/n8n/patterns/README.md rename 10_n8n_patterns_valides.md => knowledge/n8n/patterns/general.md (56%) create mode 100644 knowledge/n8n/risques/README.md rename 10_n8n_risques_et_vigilance.md => knowledge/n8n/risques/general.md (57%) create mode 100644 knowledge/product/patterns/README.md rename 10_product_patterns_valides.md => knowledge/product/patterns/general.md (71%) create mode 100644 knowledge/product/risques/README.md create mode 100644 knowledge/ux/patterns/README.md create mode 100644 knowledge/ux/risques/README.md create mode 100644 knowledge/workflow/risques/README.md create mode 100644 knowledge/workflow/risques/story-tracking.md diff --git a/00_INDEX.md b/00_INDEX.md index dccd780..1274b66 100644 --- a/00_INDEX.md +++ b/00_INDEX.md @@ -37,30 +37,18 @@ Référence portable à utiliser dans les scripts et templates : --- -## Patterns validés +## Patterns validés & Risques -Patterns éprouvés en production ou en projets réels. +Organisés dans `knowledge/` par domaine. Chaque domaine a un sous-dossier `patterns/` et `risques/`, chacun avec un `README.md` servant d'index. -- `10_backend_patterns_valides.md` -- `10_frontend_patterns_valides.md` -- `10_ux_patterns_valides.md` -- `10_product_patterns_valides.md` -- `10_n8n_patterns_valides.md` - ---- - -## Risques et anti‑patterns - -Situations connues pouvant entraîner : - -- bugs difficiles -- dette technique -- complexité inutile - -- `10_backend_risques_et_vigilance.md` -- `10_frontend_risques_et_vigilance.md` -- `10_ux_risques_et_vigilance.md` -- `10_n8n_risques_et_vigilance.md` +| Domaine | Patterns | Risques | +| ------- | -------- | ------- | +| Backend | `knowledge/backend/patterns/` | `knowledge/backend/risques/` | +| Frontend | `knowledge/frontend/patterns/` | `knowledge/frontend/risques/` | +| UX | `knowledge/ux/patterns/` | `knowledge/ux/risques/` | +| n8n | `knowledge/n8n/patterns/` | `knowledge/n8n/risques/` | +| Product | `knowledge/product/patterns/` | `knowledge/product/risques/` | +| Workflow | — | `knowledge/workflow/risques/` | --- diff --git a/10_backend_patterns_valides.md b/10_backend_patterns_valides.md deleted file mode 100644 index 568f750..0000000 --- a/10_backend_patterns_valides.md +++ /dev/null @@ -1,1435 +0,0 @@ -# Patterns back-end validés - -Ce fichier contient **uniquement** des patterns back-end : - -- testés, -- validés, -- utilisés en conditions réelles. - -Objectif : éviter de réinventer la roue et réduire le temps de debug. - -Dernière mise à jour : 23-03-2026 - ---- - -## Index - -- [Format d’erreur API standardisé](#pattern-format-derreur-api-standardise) -- [Middleware de corrélation (requestId / traceId)](#pattern-middleware-correlation-requestid-traceid) -- [Idempotency key pour opérations sensibles](#pattern-idempotency-key-operations-sensibles) -- [Pagination robuste (cursor-based) pour les listings](#pattern-pagination-robuste-cursor-based) -- [Exécution asynchrone des tâches longues (queue + outbox light)](#pattern-execution-asynchrone-taches-longues) -- [Soft delete et archivage explicite](#pattern-soft-delete-archivage-explicite) -- [Webhooks sortants robustes et idempotents](#pattern-webhooks-sortants-robustes-idempotents) -- [Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)](#pattern-contracts-first-zod-infer-no-dto) -- [Guard global NestJS — ordre d’enregistrement et décorateurs de bypass](#pattern-guard-global-nestjs) -- [Provider-Strategy pour intégrations tierces — périmètre complet](#pattern-provider-strategy-integrations-tierces) -- [Stripe — metadata sur `subscription_data`, pas sur la Session](#pattern-stripe-subscription-metadata) -- [Webhooks entrants — parsing unique (single constructWebhookEvent)](#pattern-webhook-parsing-unique) -- [Contracts-First — error codes comme contrat obligatoire](#pattern-contracts-error-codes) -- [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court) -- [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid) -- [Restauration d’achats Stripe en 3 étapes](#pattern-restauration-achats-stripe) -- [Mapping explicite de `P2002` Prisma sur create/update de champ unique](#pattern-prisma-p2002-update-unique) -- [Autorisation interne minimale sans RBAC complet](#pattern-autorisation-interne-minimale) -- [Anti-énumération sur endpoints auth liés à un email](#pattern-anti-enumeration-auth-email) -- [Token à usage unique — génération, hash et invalidation atomique](#pattern-token-usage-unique) -- [Next.js runtime-only — orchestration en bord et logique pure testable](#pattern-nextjs-runtime-only-logique-pure-testable) -- [Guardrails multi-tenant — 403 vs 404 selon la sémantique](#pattern-guardrails-multi-tenant-403-404) -- [Repository tenant-aware — `tenantId` obligatoire dans la signature](#pattern-repository-tenant-aware) -- [Défense en profondeur — inclure `tenantId` dans les updates](#pattern-tenantid-dans-updates) -- [Next.js server-only & Server Actions — règles d'isolation](#pattern-nextjs-server-only-isolation) -- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques) -- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier) -- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique) -- [Filtrage des règles métier dans le service, pas dans le repository](#pattern-filtrage-metier-service) -- [Sérialiser les champs `Decimal` Prisma en string au niveau du repository](#pattern-decimal-prisma-serialisation) -- [Extraire les helpers de résolution tenant dans un module partagé dédié](#pattern-helper-tenant-module-partage) -- [Helper centralisé d'activation de features tenant-scoped](#pattern-helper-feature-flag-tenant) -- [Réutiliser un champ existant plutôt que créer un modèle dédié en V1](#pattern-reutiliser-champ-existant-v1) -- [Valider le protocole d'une URL externe avant de la passer à un lien public](#pattern-validation-url-externe) -- [Utilitaires purs : extraire dans un module sans `server-only`](#pattern-utilitaires-purs-module-partage) -- [EN enforcement optionnel par tenant (toggle + publish gate)](#pattern-en-enforcement-tenant) -- [Prisma — Migration manuelle sans shadow DB (P3014)](#pattern-prisma-migration-manuelle-p3014) - ---- - -## Règle d’or - -Si ce n’est pas confirmé comme fonctionnel et utile, **ça n’a rien à faire ici**. - -- Pas de “bonnes pratiques” vagues -- Pas de dépendances implicites à une stack -- Si c’est spécifique à un framework / runtime / DB : on le note - ---- - -## Périmètre couvert - -- API (REST/GraphQL), services applicatifs -- authn/authz -- contrats (validation / schémas) -- gestion d’erreurs -- DB & migrations -- observabilité -- opérations sensibles (idempotence, retries) -- intégrations (webhooks, jobs async) - ---- - -## Format standard d’un pattern - -## Pattern : - -- Objectif : … -- Contexte : … -- Quand l’utiliser : … -- Quand l’éviter : … -- Avantage : … -- Limites / vigilance : … -- Validé le : DD-MM-YYYY -- Contexte technique : (obligatoire) ex. `Node 20 / Postgres 16` ou `Python 3.12 / FastAPI / Redis` - -### Implémentation (exemple minimal) - -```txt -(contenu) -``` - -### Checklist (si pertinente) - -- Erreurs standardisées -- Validation d’entrée (schéma) -- Observabilité minimale (requestId/traceId + logs) -- Sécurité (authn/authz + secrets) -- Tests au bon niveau -- Idempotence si opération sensible - ---- - - - -## Pattern : Format d’erreur API standardisé - -- Objectif : fournir des erreurs prévisibles, exploitables et cohérentes pour tous les clients. -- Contexte : API consommée par front-end, automatisations ou intégrations externes. -- Quand l’utiliser : dès qu’une API est exposée à autre chose qu’un usage interne trivial. -- Quand l’éviter : jamais. -- Avantage : - - Debug plus rapide - - UX maîtrisée côté client - - Observabilité améliorée -- Limites / vigilance : - - Discipline requise pour éviter les formats ad hoc -- Validé le : 25-01-2026 -- Contexte technique : API HTTP agnostique - -### Implémentation (exemple minimal) - -```json -{ - "error": { - "code": "USER_NOT_FOUND", - "message": "Utilisateur introuvable", - "requestId": "abc-123" - } -} -``` - -### Checklist - -- Codes HTTP cohérents (4xx / 5xx) -- Codes d’erreur applicatifs stables -- Message utilisateur non technique -- requestId présent - ---- - - - -## Pattern : Middleware de corrélation (requestId / traceId) - -- Objectif : relier chaque requête aux logs et erreurs associées. -- Contexte : toute API ou service exposé. -- Quand l’utiliser : systématiquement en production. -- Quand l’éviter : jamais. -- Avantage : - - MTTR réduit drastiquement - - Debug cross-services possible -- Limites / vigilance : - - Doit être propagé partout (logs, erreurs, appels sortants) -- Validé le : 25-01-2026 -- Contexte technique : Backend agnostique (HTTP) - -### Implémentation (exemple minimal) - -```txt -- Générer un requestId à l’entrée si absent -- Le propager dans le contexte de requête -- L’inclure dans chaque log et réponse d’erreur -``` - -### Checklist - -- requestId généré ou repris d’un header existant -- Présent dans tous les logs -- Présent dans les erreurs retournées - ---- - - - -## Pattern : Idempotency key pour opérations sensibles - -- Objectif : empêcher les doublons lors de retries ou timeouts. -- Contexte : création de ressources, paiements, webhooks. -- Quand l’utiliser : toute opération non strictement en lecture. -- Quand l’éviter : endpoints purement GET. -- Avantage : - - Protection contre doublons - - Robustesse face aux retries -- Limites / vigilance : - - Stockage et expiration des clés à gérer -- Validé le : 25-01-2026 -- Contexte technique : API HTTP + DB transactionnelle - -### Implémentation (exemple minimal) - -```txt -- Client fournit Idempotency-Key -- Backend stocke la clé + résultat -- Retry retourne le résultat initial -``` - -### Checklist - -- Clé obligatoire sur endpoints sensibles -- Contrainte d’unicité côté DB -- Comportement documenté - ---- - - - -## Pattern : Pagination robuste (cursor-based) pour les listings - -- Objectif : fournir des listings stables et performants sans incohérences entre pages. -- Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important. -- Quand l’utiliser : dès qu’un listing peut dépasser quelques dizaines/centaines d’items ou subir des écritures concurrentes. -- Quand l’éviter : listes strictement petites et statiques. -- Avantage : - - Résultats stables malgré insertions/suppressions - - Meilleure performance que l’offset sur gros volumes - - Expérience client plus fiable -- Limites / vigilance : - - Nécessite un tri déterministe (champ + tie-breaker) - - Complexité légèrement supérieure à offset/limit -- Validé le : 25-01-2026 -- Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework - -### Implémentation (exemple minimal) - -```txt -- Trier par (createdAt DESC, id DESC) (exemple) -- Le client envoie cursor = dernier (createdAt,id) reçu -- Le backend renvoie nextCursor si plus de résultats -- Ne jamais exposer de cursor implicite ou non documenté -``` - -### Checklist - -- Tri déterministe (avec tie-breaker) -- nextCursor renvoyé et documenté -- Limite max de page (protection) -- Index DB aligné avec le tri - ---- - - - -## Pattern : Exécution asynchrone des tâches longues (queue + outbox light) - -- Objectif : sortir les opérations longues ou fragiles du chemin request/response. -- Contexte : envoi d’emails, appels SaaS, génération de PDF, traitements batch, webhooks sortants. -- Quand l’utiliser : dès qu’une opération peut dépasser la latence acceptable ou dépendre d’un service externe. -- Quand l’éviter : opérations réellement instantanées et sans dépendances externes. -- Avantage : - - API plus rapide et plus fiable - - Retries maîtrisés - - Meilleure résilience aux pannes externes -- Limites / vigilance : - - Demande une discipline stricte sur l’idempotence - - Nécessite une stratégie minimale de dead-letter ou d’alerting -- Validé le : 25-01-2026 -- Contexte technique : Backend agnostique + DB transactionnelle + worker - -### Implémentation (exemple minimal) - -```txt -- API écrit un job ou event en DB dans la transaction métier -- Worker lit les jobs en attente et exécute -- Retries avec backoff + compteur -- Statut FAILED ou dead-letter + alerte -- Idempotence par clé métier ou idempotency key -``` - -### Checklist - -- Job créé dans une transaction (évite les pertes) -- Retries et backoff définis -- Dead-letter ou statut FAILED visible -- Idempotence garantie -- Logs corrélés (requestId/traceId) - ---- - - - -## Pattern : Soft delete et archivage explicite - -- Objectif : permettre la suppression logique sans perte immédiate de données. -- Contexte : données métier critiques, besoins d’audit, restauration ou conformité. -- Quand l’utiliser : dès qu’une suppression peut avoir des impacts métier ou légaux. -- Quand l’éviter : données purement techniques ou réellement éphémères. -- Avantage : - - Restauration possible - - Audit et traçabilité - - Réduction des suppressions irréversibles -- Limites / vigilance : - - Complexité accrue sur les requêtes - - Nécessite une discipline stricte (filtres par défaut) -- Validé le : 25-01-2026 -- Contexte technique : API + DB relationnelle - -### Implémentation (exemple minimal) - -```txt -- Champ deletedAt (nullable) ou status -- Les requêtes standards filtrent deletedAt IS NULL -- Endpoints dédiés pour restauration / purge -- Index DB tenant compte du soft delete -``` - -### Checklist - -- Filtrage soft delete par défaut -- Restauration explicite possible -- Purge maîtrisée (cron / job) -- Index DB adaptés -- Tests sur cas supprimé / restauré - ---- - - - -## Pattern : Webhooks sortants robustes et idempotents - -- Objectif : garantir des intégrations fiables avec des systèmes externes. -- Contexte : notifications, synchronisations, événements métier sortants. -- Quand l’utiliser : dès qu’un événement doit être transmis à un tiers. -- Quand l’éviter : intégrations strictement synchrones et internes. -- Avantage : - - Tolérance aux pannes réseau - - Retries maîtrisés - - Observabilité des échecs -- Limites / vigilance : - - Gestion des retries et du volume - - Nécessite une idempotence côté consommateur -- Validé le : 25-01-2026 -- Contexte technique : Backend + HTTP + worker/queue - -### Implémentation (exemple minimal) - -```txt -- Événement persisté (outbox) en DB -- Envoi asynchrone via worker -- Retries avec backoff -- Signature du payload (HMAC) -- Idempotency key dans le header -``` - -### Checklist - -- Payload signé et vérifiable -- Retries + backoff définis -- Dead-letter ou statut FAILED visible -- Idempotence documentée -- Logs corrélés (requestId/traceId) - ---- - - - -## Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack) - -- Objectif : avoir une seule source de vérité pour les contrats d’interface entre API et client, sans redéfinition manuelle de types. -- Contexte : monorepo TypeScript avec un package partagé (`packages/contracts` ou équivalent), consommé par le backend et le front/mobile. -- Quand l’utiliser : dès qu’une API est consommée par un client TypeScript dans le même repo. -- Quand l’éviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence. -- Avantage : - - Zéro drift entre contrat et implémentation - - Types TypeScript gratuits via `z.infer<>` — aucune réécriture - - Changement de contrat = erreur de compilation immédiate côté client - - Mocks de tests alignés automatiquement -- Limites / vigilance : - - Ne pas mettre de logique métier dans `packages/contracts` (IO only) - - Attention aux dépendances circulaires si le package grossit -- Validé le : 07-03-2026 -- Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework - -### Implémentation (exemple minimal) - -```typescript -// packages/contracts/src/auth/auth.schemas.ts -export const RegisterRequestSchema = z.object({ - email: z.string().email(), - password: z.string().min(8), -}); -export type RegisterRequest = z.infer; // type GRATUIT - -// packages/contracts/src/index.ts -export * from ‘./auth/auth.schemas’; -export * from ‘./errors/error-code’; - -// apps/api/src/modules/auth/auth.controller.ts -import type { RegisterRequest } from ‘@monrepo/contracts’; -// + ZodValidationPipe → validation automatique, zéro DTO manuel - -// apps/mobile/src/domains/auth/auth.store.ts -import type { RegisterRequest } from ‘@monrepo/contracts’; -// même type, même schéma, zéro duplication -``` - -### Structure cible du package contracts - -``` -packages/contracts/src/ - auth/auth.schemas.ts ← request/response auth - users/users.schemas.ts ← request/response users - billing/billing.schemas.ts ← request/response billing (Epic suivant) - errors/error-code.ts ← enum codes d’erreur stables - http/envelopes.ts ← { data, meta } / { error, meta } - index.ts ← re-export tout -``` - -### Ce qui appartient à contracts - -- Schémas Zod request/response -- Types inférés (`z.infer<>`) -- Codes d’erreur applicatifs stables -- Enums et constantes partagées (ex : liste officielle de sujets/topics) - -### Ce qui n’appartient PAS à contracts - -- Logique métier -- Modules/services/guards framework (NestJS, etc.) -- State management client (Zustand, Redux, etc.) - -### Checklist - -- [ ] Zéro DTO manuel dans l’API — uniquement `z.infer` -- [ ] `ZodValidationPipe` global ou par endpoint pour la validation d’entrée -- [ ] Constantes partagées (enums, listes) dans contracts, jamais dupliquées -- [ ] Mocks de tests importent les types depuis contracts - ---- - - - -## Pattern : Guard global NestJS — ordre d’enregistrement et décorateurs de bypass - -- Objectif : protéger tous les endpoints par défaut, avec un mécanisme explicite pour les exceptions. -- Contexte : API NestJS avec plusieurs guards globaux (authn, authz, feature flags...). -- Quand l’utiliser : dès qu’on a 2+ guards globaux dont l’un dépend du résultat de l’autre. -- Quand l’éviter : si un seul guard suffit. -- Avantage : - - Sécurité par défaut (opt-out, pas opt-in) - - Ordre d’exécution garanti et explicite - - Bypass documenté et traçable via décorateurs -- Limites / vigilance : - - L’ordre des `APP_GUARD` dans `providers[]` est l’ordre d’exécution — ne pas inverser - - Exporter le service depuis son module si injecté dans un guard global d’un autre module -- Validé le : 07-03-2026 -- Contexte technique : NestJS v10+ - -### Implémentation (exemple minimal) - -```typescript -// app.module.ts -providers: [ - { provide: APP_GUARD, useClass: AuthGuard }, // 1er : peuple request.user - { provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user - { provide: APP_GUARD, useClass: EntitlementsGuard }, // 3ème : lit request.user + entitlements -] - -// skip-auth.decorator.ts -export const SKIP_AUTH = ‘skipAuth’; -export const SkipAuth = () => SetMetadata(SKIP_AUTH, true); - -// auth.guard.ts -const skip = this.reflector.getAllAndOverride(SKIP_AUTH, [ - context.getHandler(), - context.getClass(), // permet @SkipAuth() au niveau classe -]); -if (skip) return true; -``` - -### Checklist - -- [ ] AuthGuard enregistré en premier dans `providers[]` -- [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule -- [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs) -- [ ] Tests unitaires sur le guard avec reflector mocké - ---- - - - -## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet - -- Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers. -- Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…). -- Quand l’utiliser : dès qu’un service applicatif dépend d’un SDK tiers (et plus encore s’il y a des webhooks). -- Quand l’éviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle. -- Avantage : - - Testabilité : mock du provider, pas du SDK - - Remplacement du prestataire sans refactor “en cascade” - - Responsabilités claires : provider = “parle Stripe”, service = “parle domaine” -- Limites / vigilance : - - L’interface doit exposer des **types normalisés** (pas de types Stripe) - - Le provider gère aussi les webhooks : validation signature, parsing event, mapping -- Validé le : 09-03-2026 -- Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable - -### Implémentation (exemple minimal) - -```typescript -// billing-provider.interface.ts (pas d'import Stripe) -export type BillingPlan = 'MONTHLY' | 'ANNUAL'; - -export type BillingWebhookResult = { - userId: string; - externalId: string; - plan: BillingPlan; - status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED'; - currentPeriodEnd: Date | null; -}; - -export interface BillingProvider { - createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>; - cancelSubscription(externalId: string): Promise; - handleWebhook(rawBody: Buffer, signature: string): Promise; -} - -// billing.service.ts (domaine uniquement) -async handleWebhook(rawBody: Buffer, signature: string): Promise { - const result = await this.billingProvider.handleWebhook(rawBody, signature); - if (!result) return; - await this.prisma.subscription.upsert({ /* données normalisées */ }); -} -``` - ---- - - - -## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session - -- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`. -- Contexte : intégration Stripe Checkout avec webhooks abonnement. -- Quand l’utiliser : systématiquement dès qu’on crée une Checkout Session liée à une Subscription. -- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod. -- Validé le : 09-03-2026 -- Contexte technique : Stripe API v17+ / NestJS - -### Implémentation - -```typescript -stripe.checkout.sessions.create({ - metadata: { userId }, // pour checkout.session.completed - subscription_data: { metadata: { userId } }, // pour customer.subscription.* -}); -``` - ---- - - - -## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`) - -- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs. -- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…). -- Quand l’utiliser : dès qu’on a 2+ handlers webhook sur le même endpoint. -- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO). -- Validé le : 09-03-2026 -- Contexte technique : Stripe / NestJS - -### Implémentation - -```typescript -// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque -// 2. Extracteurs purs, sans effet de bord : -handleSubscriptionWebhookEvent(event): WebhookResult | null -handlePackWebhookEvent(event): PackWebhookResult | null -// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats -``` - ---- - - - -## Pattern : Contracts-First — error codes comme contrat obligatoire - -- Objectif : maintenir les codes d’erreur API dans `packages/contracts` pour éviter les clients stringly-typed. -- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`. -- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après. -- Risque si ignoré : clients qui testent des strings hardcodées au lieu d’importer l’enum → drift silencieux. -- Validé le : 09-03-2026 -- Contexte technique : TypeScript / NestJS + Expo (React Native) - -### Checklist - -- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit -- [ ] Clients importent l’enum, pas une string littérale -- [ ] PR review : vérifier `error-code.ts` à chaque ajout d’endpoint d’erreur - ---- - - - -## Pattern : RedisHealthService avec cache interne court - -- Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête. -- Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision d’écriture. -- Quand l’utiliser : quand plusieurs requêtes concurrentes doivent consulter l’état Redis. -- Quand l’éviter : si Redis n’est pas consulté dans le chemin request/response. -- Avantage : - - réduit fortement le flood de `PING` - - garde un signal d’état suffisamment frais -- Limites / vigilance : - - la fenêtre de cache doit rester courte - - l’état initial doit être explicite et assumé -- Validé le : 10-03-2026 -- Contexte technique : NestJS / Redis - -### Implémentation (exemple minimal) - -```txt -- Mémoriser lastStatus et lastCheck -- Si le dernier check a moins de 5s, retourner l’état en cache -- Sinon exécuter un vrai PING et mettre le cache à jour -- Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot -``` - -### Checklist - -- Cache court documenté -- Pas de ping Redis à chaque requête -- Comportement initial explicite - ---- - - - -## Pattern : Sémantique explicite `Trial` vs `Paid` dans Subscription - -- Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de l’abonnement payant actif. -- Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai. -- Quand l’utiliser : dès qu’un même enregistrement supporte trial et abonnement payant. -- Quand l’éviter : si trial et abonnement payant sont modélisés par des entités distinctes. -- Avantage : - - évite les incohérences silencieuses dans les guards - - rend les fixtures et mocks e2e cohérents avec la règle métier -- Limites / vigilance : - - toute logique `isActive` doit préciser si elle signifie “trial ou paid” ou “paid only” -- Validé le : 10-03-2026 -- Contexte technique : Backend agnostique / modèle d’abonnement - -### Implémentation (exemple minimal) - -```txt -- Un abonnement payant actif n’est pas seulement status = ACTIVE -- Il doit aussi avoir trialEndsAt = null -- Les fixtures et mocks e2e d’un abonnement payant fixent toujours trialEndsAt: null -``` - -### Checklist - -- Règle métier explicitée -- Guards alignés sur la sémantique choisie -- Fixtures et seeds cohérents - ---- - - - -## Pattern : restauration d’achats Stripe en 3 étapes - -- Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre d’une hypothèse fragile. -- Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé. -- Quand l’utiliser : dès qu’un utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation. -- Quand l’éviter : si l’état Stripe n’est pas la source de vérité. -- Avantage : - - rend la réconciliation explicite - - supporte retries et restaurations tardives -- Limites / vigilance : - - la pagination Stripe et l’idempotence d’écriture restent obligatoires -- Validé le : 10-03-2026 -- Contexte technique : Stripe API / backend Node/NestJS - -### Implémentation (exemple minimal) - -```txt -1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent) -2. Reconstruction de l’état Stripe utile au domaine -3. Réconciliation et écritures locales idempotentes -``` - -### Checklist - -- `stripeCustomerId` persistant côté app -- Réconciliation explicite documentée -- Upsert ou écriture idempotente - ---- - - - -## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique - -- Objectif : transformer un conflit d’unicité prévisible en erreur métier exploitable plutôt qu’en 500 opaque. -- Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check. -- Quand l’utiliser : dès qu’un champ unique peut entrer en collision — à la création ET à la modification. -- Quand l’éviter : jamais si le champ peut réellement entrer en collision. -- Avantage : - - réponse client stable - - diagnostic métier plus rapide -- Limites / vigilance : - - le mapping doit rester cohérent avec le format d’erreur API standardisé -- Validé le : 10-03-2026 -- Contexte technique : Prisma / PostgreSQL / NestJS - -### Implémentation (exemple minimal) - -```txt -- Catch explicite de PrismaClientKnownRequestError code P2002 -- Mapping vers une erreur métier stable -- Conserver requestId et format d’erreur standardisé -``` - -### Implémentation (exemple complet) - -```typescript -import { Prisma } from "@prisma/client"; - -try { - await prisma.item.create({ data: { ... } }); - // ou: await prisma.item.update({ where: { id }, data: { ... } }); -} catch (err) { - if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { - throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 }); - } - throw err; -} -``` - -**Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. S’applique à `create`, `update`, `updateMany`, `upsert`. - -### Checklist - -- `P2002` intercepté sur les creates ET les updates sensibles -- Code d’erreur métier stable (409 Conflict) -- Pas de 500 générique sur conflit prévisible - ---- - - - -## Pattern : Autorisation interne minimale sans RBAC complet - -- Objectif : sécuriser une capacité interne sensible sans ouvrir trop tôt un chantier RBAC complet. -- Contexte : application avec peu de rôles, besoin ponctuel d’une capacité admin ou opérateur clairement identifiée. -- Quand l’utiliser : quand une story métier demande un pouvoir interne limité mais réel. -- Quand l’éviter : si les permissions deviennent nombreuses, hiérarchiques ou contextuelles. -- Avantage : - - sécurisation rapide et lisible d’une capacité sensible - - source de vérité backend explicite - - chemin d’évolution propre vers un RBAC plus complet -- Limites / vigilance : - - ne pas laisser proliférer des rôles ad hoc non gouvernés - - ne remplace pas un vrai modèle de permissions si le domaine grossit -- Validé le : 10-03-2026 -- Contexte technique : NestJS / auth par session ou JWT / API métier interne - -### Implémentation (exemple minimal) - -```txt -- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN) -- propager ce rôle dans la session ou le token d’auth -- créer un décorateur + guard dédiés pour la capacité sensible -- interdire les booléens front, emails hardcodés ou `if` dispersés dans les contrôleurs -``` - -### Checklist - -- Le rôle vit dans la source de vérité backend -- Le rôle est propagé dans le mécanisme d’auth existant -- Les endpoints sensibles passent par un guard dédié -- Aucun contrôle d’accès critique n’est piloté par le front -- Le passage à RBAC reste possible sans casser le contrat existant - ---- - -### Notes importantes - -- On préfère 5 patterns solides à 50 “bons conseils”. -- Un pattern = une idée actionnable + son cadre d’utilisation. - ---- - - -## Pattern : Anti-énumération sur endpoints auth liés à un email - -- Objectif : empêcher qu’un endpoint auth révèle si un compte existe, n’existe pas ou n’est pas éligible. -- Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part d’un email utilisateur. -- Quand l’utiliser : dès qu’une requête auth touche un identifiant de type email. -- Quand l’éviter : jamais sur une surface exposée. -- Avantage : - - réduit la fuite d’information sur les comptes existants - - homogénéise les réponses côté client - - se combine bien avec les garde-fous anti-abus -- Limites / vigilance : - - ne protège pas seul contre le brute-force, à combiner avec du rate-limiting - - les logs internes doivent conserver la vraie cause sans l’exposer au client -- Validé le : 16-03-2026 -- Contexte technique : Node.js / auth applicative / API HTTP - -### Implémentation (exemple minimal) - -```txt -- retourner la même réponse HTTP 200 qu’un compte existe ou non -- ne jamais distinguer "email inconnu", "email connu" ou "compte OAuth-only" dans la réponse -- journaliser la cause réelle côté serveur -- ajouter un rate-limiting basé sur email + IP -``` - -### Checklist - -- Réponse client uniforme pour les cas compte connu/inconnu/non éligible -- Aucune fuite d’existence dans le message ou le code d’erreur -- Rate-limiting présent sur les endpoints exposés -- Logs internes exploitables - ---- - - -## Pattern : Token à usage unique — génération, hash et invalidation atomique - -- Objectif : standardiser la création et la consommation de tokens sensibles sans stocker de secret brut en base. -- Contexte : invitation, reset de mot de passe, vérification d’email, lien magique ou tout token one-shot. -- Quand l’utiliser : pour tout token à usage unique transmis à l’utilisateur. -- Quand l’éviter : sessions longues ou secrets devant être relus en clair côté serveur. -- Avantage : - - réduit l’impact d’une fuite de base - - garde des tokens URL-safe - - favorise une consommation atomique et réutilisable -- Limites / vigilance : - - la consommation doit rester atomique - - la politique d’expiration doit être explicite -- Validé le : 16-03-2026 -- Contexte technique : Node.js `crypto` / Prisma / email ou URL signée - -### Implémentation (exemple minimal) - -```txt -- générer le token avec `crypto.randomBytes(32).toString("base64url")` -- stocker uniquement le hash SHA-256 du token en base -- transmettre le token brut uniquement via URL ou email -- recalculer le hash côté serveur lors de la consommation -- invalider le token dans une transaction atomique après usage -``` - -### Checklist - -- Token brut jamais persisté en base -- Hash recalculé côté serveur pour la vérification -- Expiration explicite -- Invalidation atomique après consommation - ---- - - -## Pattern : Next.js runtime-only — orchestration en bord et logique pure testable - -- Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d’orchestration. -- Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node. -- Quand l’utiliser : dès qu’un flux serveur mélange APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`, `server-only`) et logique métier réutilisable. -- Quand l’éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément. -- Avantage : - - garde la logique métier importable dans un runner Node standard - - évite que `server-only` contamine des modules purs - - facilite les tests unitaires sans mocks lourds du runtime Next.js - - clarifie la responsabilité des Server Actions et handlers serveur -- Limites / vigilance : - - demande une discipline de découpage - - peut introduire une indirection inutile si la logique extraite est réellement triviale - - les frontières d’injection doivent rester simples pour éviter un excès d’abstraction -- Validé le : 19-03-2026 -- Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables - -### Implémentation (exemple minimal) - -```txt -- réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js -- garder la Server Action, route handler ou module email comme couche d’orchestration fine -- extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only` -- injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.) -- tester unitairement le module pur dans le runner Node ; tester l’orchestrateur plus légèrement -``` - -### Checklist - -- `server-only` absent des modules de logique pure -- APIs Next.js runtime-only limitées aux couches d’entrée -- Logique métier principale testable sans runtime Next.js -- Dépendances injectées explicitement quand utile -- Server Action ou handler fin et lisible - ---- - - -## Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique - -- Objectif : éviter les fuites d’information inter-tenant tout en gardant une sémantique d’erreur claire. -- Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur. -- Quand l’utiliser : dès qu’une vérification d’appartenance tenant peut soit refuser explicitement l’accès, soit masquer l’existence d’une ressource. -- Quand l’éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite. -- Avantage : - - clarifie la convention de sécurité - - évite les réponses incohérentes selon les modules - - facilite les tests d’isolation tenant -- Limites / vigilance : - - la convention doit être documentée et appliquée partout - - un mauvais choix entre 403 et 404 peut révéler une information sensible -- Validé le : 16-03-2026 -- Contexte technique : API multi-tenant / HTTP / services métier - -### Implémentation (exemple minimal) - -```txt -- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l’accès refusé -- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l’existence d’une ressource d’un autre tenant -- documenter la convention dans le module -- couvrir les deux sémantiques par des tests dédiés -``` - -### Checklist - -- Convention 403 vs 404 documentée -- Helpers distincts selon la sémantique métier -- Aucune fuite d’existence cross-tenant sur les ressources métier -- Tests dédiés sur les deux comportements - ---- - - -## Pattern : Repository tenant-aware — `tenantId` obligatoire dans la signature - -- Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant. -- Contexte : repositories ou services d’accès aux données sur ressources tenant-scoped. -- Quand l’utiliser : dès qu’un domaine métier est massivement filtré par tenant. -- Quand l’éviter : domaines réellement globaux ou méthodes volontairement cross-tenant. -- Avantage : - - force le scoping dès la signature TypeScript - - réduit les oublis de filtre tenant dans les call sites - - rend les exceptions cross-tenant visibles -- Limites / vigilance : - - les exceptions cross-tenant doivent être rares et documentées explicitement - - ne dispense pas d’un second garde-fou dans les mutations sensibles -- Validé le : 16-03-2026 -- Contexte technique : TypeScript / Prisma / architecture repository - -### Implémentation (exemple minimal) - -```txt -- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire -- les méthodes réellement cross-tenant sont nommées et documentées comme exception -- les call sites Prisma directs sur ces domaines sont interdits ou supprimés -``` - -### Checklist - -- `tenantId` obligatoire sur les méthodes tenant-scoped -- Exceptions cross-tenant documentées -- Appels directs concurrents à Prisma supprimés -- Tests sur scoping tenant au niveau repository - ---- - - -## Pattern : Défense en profondeur — inclure `tenantId` dans les updates - -- Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont. -- Contexte : `update` ou `updateMany` sur une ressource tenant-scoped. -- Quand l’utiliser : dès qu’une mutation dépend d’un `id` reçu ou résolu dans un flux multi-tenant. -- Quand l’éviter : ressources globales non liées à un tenant. -- Avantage : - - ajoute une seconde barrière côté base - - réduit l’impact d’un call site mal scopé - - rend la mutation plus sûre sans complexité forte -- Limites / vigilance : - - ne remplace pas le scoping en lecture ni la vérification d’autorisation - - suppose que `tenantId` soit disponible au moment de la mutation -- Validé le : 16-03-2026 -- Contexte technique : Prisma / multi-tenant / mutations métier - -### Implémentation (exemple minimal) - -```txt -- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped -- appliquer la même règle sur `updateMany` et opérations de révocation -- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité -``` - -### Checklist - -- `tenantId` présent dans les clauses `where` des updates sensibles -- Pas de mutation tenant-scoped basée sur `id` seul -- Revue explicite des exceptions documentées - ---- - - -## Pattern : Next.js server-only & Server Actions — règles d'isolation - -- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires. -- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif. -- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js. -- Quand l'éviter : modules purement UI côté client. -- Avantage : - - logique pure testable sans friction (runner Node natif) - - Server Action fine et lisible — orchestration uniquement - - `server-only` explicite et intentionnel, pas par habitude -- Limites / vigilance : - - ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js -- Validé le : 16-03-2026 -- Contexte technique : Next.js App Router / Node.js test runner - -### Règles - -```txt -- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime - (cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure -- Logique pure extraite dans un module injectable sans `server-only` : - deleteSession({ prismaClient, sessionToken }) - → testable avec le runner Node sans friction -- Server Action = orchestration mince, elle appelle les modules purs injectés - et gère les dépendances Next.js runtime uniquement -- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé, - sans import nodemailer / server-only -``` - -### Checklist - -- [ ] `server-only` absent des repositories et modules de logique pure -- [ ] Server Action ≤ 10 lignes, délègue au module pur injectable -- [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale -- [ ] La logique pure ne dépend pas du runtime pour être exécutée - ---- - - -## Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes - -- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent. -- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session. -- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence. -- Quand l'éviter : opérations de lecture pure. -- Avantage : - - pas de token valide après reset de mot de passe si l'opération est interrompue - - suppression de session idempotente (P2025 absorbé silencieusement) - - comportement prévisible même en cas de retry ou de concurrence -- Limites / vigilance : - - `$transaction` Prisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction -- Validé le : 16-03-2026 -- Contexte technique : Node.js / Prisma / auth par session ou token - -### Implémentation (exemple minimal) - -```typescript -// consumePasswordReset — atomique dans une transaction -await prisma.$transaction([ - prisma.passwordResetToken.update({ - where: { tokenHash }, - data: { consumedAt: new Date() }, - }), - prisma.user.update({ - where: { id: userId }, - data: { passwordHash: newHash }, - }), - prisma.session.deleteMany({ where: { userId } }), -]); - -// Suppression de session — idempotente (P2025 absorbé) -try { - await prisma.session.delete({ where: { sessionToken } }); -} catch (err) { - if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK -} -``` - -### Checklist - -- [ ] Toute opération hash + update + delete dans une `$transaction` -- [ ] `P2025` absorbé silencieusement sur les suppressions de session -- [ ] Effets de bord hors transaction documentés (cookie, email) -- [ ] Tests couvrant le cas d'une session déjà expirée - ---- - - -## Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès - -- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client. -- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur. -- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu. -- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404). -- Avantage : - - pas de gestion d'exception côté client mobile pour des états courants - - rendu conditionnel (paywall, teaser, empty) piloté par le payload - - log serveur propre — 4xx réservés aux erreurs techniques/sécurité -- Limites / vigilance : - - ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP -- Validé le : 20-03-2026 -- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1 - -### Implémentation (exemple minimal) - -```typescript -// GET /community/forums -// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta } -// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta } - -// ❌ Anti-pattern -return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } }); - -// ✅ Pattern correct -return res.status(200).json({ - data: { forums: [], paywallRequired: true }, - meta: { total: 0 }, -}); -``` - -### Règle - -- **4xx** = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable) -- **200 + flag métier** = état métier normal que le client doit interpréter pour le rendu - ---- - - -## Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline) - -- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline. -- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles). -- Quand l'utiliser : toute limite d'action journalière avec Redis disponible. -- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation). -- Avantage : - - atomicité garantie : `INCR + EXPIREAT` dans un pipeline `MULTI/EXEC` - - pas de clé sans TTL même en cas de deux requêtes simultanées (`count === 1` concurrent) - - mode dégradé explicite si Redis down (`count === null` → permissif) -- Limites / vigilance : - - compensation `incrBy(-1)` en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification - - la fenêtre expire à minuit UTC, pas à minuit local -- Validé le : 20-03-2026 -- Contexte technique : Redis / NestJS / app-alexandrie story 4.2 - -### Implémentation (exemple minimal) - -```typescript -// RedisService — méthode dédiée -async incrWithExpireAt(key: string, expireAtMs: number): Promise { - const pipeline = this.client.multi(); - pipeline.incr(key); - pipeline.expireAt(key, Math.floor(expireAtMs / 1000)); - const results = await pipeline.exec(); - return results[0] as number; // valeur post-INCR -} - -// Service métier -const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC -const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1)); -const quotaKey = `app:quota:post:${userId}:${today}`; -const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime()); - -if (count !== null && count > QUOTA_MAX) { - await redis.incrBy(quotaKey, -1); // compensation - throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS); -} -// count === null → Redis down → mode dégradé permissif -``` - -### Checklist - -- [ ] Vérifier le quota AVANT la création en DB -- [ ] `INCR + EXPIREAT` dans un pipeline atomique -- [ ] Mode dégradé permissif si `count === null` (Redis down) -- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC) -- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent) - ---- - - -## Pattern : Filtrage des règles métier dans le service, pas dans le repository - -- Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service). -- Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin). -- Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel. -- Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`. -- Avantage : - - la règle est testable unitairement sans Prisma (mock de données brutes) - - la requête DB reste simple et stable entre contextes - - les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête -- Validé le : 17-03-2026 -- Contexte technique : Prisma / Node.js / Next.js — app-template-resto - -### Implémentation (exemple minimal) - -```typescript -// Repository — charge tout ce qui est candidat -async findCategories(tenantId: string) { - return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible -} - -// Service — applique la règle métier et mappe vers DTO -const raw = await repo.findCategories(tenantId); -return raw.filter(c => c.isVisible).map(toPublicDto); - -// Admin : même repo, filtre différent dans le service admin -return raw.map(toAdminDto); // retourne tout, visible ou non -``` - ---- - - -## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository - -- Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses. -- Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action. -- Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories. -- Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner. -- Validé le : 17-03-2026 -- Contexte technique : Prisma / Node.js — app-template-resto - -### Implémentation - -```typescript -// Repository — convertir avant de retourner -return { - ...dish, - price: dish.price?.toString() ?? null, // Decimal → string -}; - -// DTO public -type DishDto = { - price: string | null; // pas Decimal -}; -``` - ---- - - -## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié - -- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant. -- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier. -- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier. -- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct. -- Validé le : 17-03-2026 -- Contexte technique : Next.js / TypeScript — app-template-resto - -### Implémentation - -```typescript -// ✅ src/server/tenant/resolvePublicTenant.ts -export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... } - -// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire -export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant"; -``` - ---- - - -## Pattern : Helper centralisé d'activation de features tenant-scoped - -- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur. -- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations). -- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant. -- Avantage : - - helper pur et testable sans I/O - - comportement par défaut sain (`null`/`undefined` → tout activé) - - composants de navigation et pages importent ce helper, jamais Prisma directement -- Validé le : 17-03-2026 -- Contexte technique : Next.js App Router / TypeScript — app-template-resto - -### Implémentation - -```typescript -// src/server/public/publicPagesConfig.ts -export function isPublicPageEnabled( - config: PublicPagesConfigRecord | null | undefined, - pageKey: PublicPageKey -): boolean { - if (!config) return true; // config absente = tout activé par défaut - return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]]; -} -``` - -**Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée. - ---- - - -## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1 - -- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple. -- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique). -- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations. -- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes. -- Avantage : zéro migration supplémentaire, zéro scope creep -- Validé le : 17-03-2026 -- Contexte technique : Prisma / Node.js — app-template-resto - -### Règle - -```txt -- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit -- Un champ optionnel dans le modèle le plus proche est suffisant en V1 -- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples -``` - ---- - - -## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public - -- Objectif : prévenir les injections `javascript:` et URLs malformées dans les `` ou `` publics. -- Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML. -- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML. -- Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS. -- Validé le : 17-03-2026 -- Contexte technique : Node.js / Next.js — app-template-resto - -### Implémentation - -```typescript -function isSafeUrl(url: string): boolean { - try { - const { protocol } = new URL(url); - return protocol === "https:" || protocol === "http:"; - } catch { - return false; - } -} - -// Validation complète en service/repository -if (mediaUrl) { - try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); } - if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://")) - throw new HttpError("URL doit commencer par https://.", { status: 400 }); - if (mediaUrl.length > 500) - throw new HttpError("URL trop longue.", { status: 400 }); -} -// Retourner null si invalide — le composant gère l'absence d'URL -``` - -### Checklist - -- [ ] Validation format (`new URL()`) + protocole + longueur max -- [ ] Retourner `null` si invalide, jamais passer la string brute -- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée - ---- - - -## Pattern : Utilitaires purs — extraire dans un module sans `server-only` - -- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction. -- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`. -- Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests. -- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle. -- Validé le : 21-03-2026 -- Contexte technique : Node.js / Next.js — app-template-resto - -### Implémentation - -``` -src/server/menuAdmin/ - allergensRepository.ts ← import { slugify } from "./slugify" - slugify.ts ← export function slugify() {} // pas de "server-only" - -tests/ - allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts" -``` - ---- - - -## Pattern : EN enforcement optionnel par tenant (toggle + publish gate) - -- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication. -- Contexte : app multi-tenant avec internationalisation optionnelle. -- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n. -- Validé le : 21-03-2026 -- Contexte technique : Prisma / Next.js App Router — app-template-resto - -### Implémentation - -```typescript -// 1. Modèle Tenant -enableEn Boolean @default(false) - -// 2. Vérification dans chaque action mutante (create/update) -const { enableEn } = await getEnConfig(tenantId); -if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 }); - -// 3. Gate publish — vérification de complétude -const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all -// Exclut : isSystem:true, tenantId:null, isVisible:false -if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 }); -``` - -**Règles :** -- `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication) -- `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`) - ---- - - -## Pattern : Prisma — Migration manuelle sans shadow DB (P3014) - -- Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes). -- Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser. -- Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`. -- Risque si ignoré : blocage complet de la migration sur env managé. -- Validé le : 23-03-2026 -- Contexte technique : Prisma v7+ — app-alexandrie / Supabase - -### Implémentation - -```bash -# 1. Écrire le SQL manuellement -mkdir -p prisma/migrations/_ -# Créer migration.sql à la main - -# 2. Appliquer le SQL directement en DB -npx prisma db execute --file prisma/migrations/_/migration.sql - -# 3. Marquer la migration comme appliquée dans _prisma_migrations -npx prisma migrate resolve --applied _ - -# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts -``` - -**Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations. diff --git a/10_backend_risques_et_vigilance.md b/10_backend_risques_et_vigilance.md deleted file mode 100644 index a14c24f..0000000 --- a/10_backend_risques_et_vigilance.md +++ /dev/null @@ -1,1145 +0,0 @@ -# Back-end — Risques & vigilance - -Ce fichier recense des risques back-end susceptibles de provoquer : - -- incidents prod, -- failles de sécurité, -- bugs non diagnostiquables, -- régressions coûteuses, -- incohérences de données. - -Dernière mise à jour : 24-03-2026 - ---- - -## Règles d’utilisation - -- Chaque entrée doit dire : - - ce qui peut mal se passer, - - comment on le voit (symptômes), - - comment on le maîtrise (mitigation). -- Si c’est lié à une stack / version : on note le contexte. - ---- - -## Index - -- [AuthN/AuthZ dispersée](#risque-authn-authz-dispersee) -- [Guard global manquant (request.user)](#risque-guard-global-manquant) -- [Duplication silencieuse de constantes (contracts)](#risque-duplication-constantes-contracts) -- [Contrats API implicites](#risque-contrats-api-implicites) -- [Erreurs non standardisées](#risque-erreurs-non-standardisees) -- [Migrations risquées / non reproductibles](#risque-migrations-risquees) -- [Non-idempotence sur opérations sensibles](#risque-non-idempotence) -- [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end) -- [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable) -- [Observabilité insuffisante](#risque-observabilite-insuffisante) -- [Webhooks entrants — répondre 200 pendant `processing` (event perdu)](#risque-webhook-200-processing) -- [Redis — thrash de connexion sous charge](#risque-redis-thrash-connexion) -- [Entitlements — TTL cache supérieur au SLA de propagation](#risque-entitlements-ttl-sla) -- [Guard NestJS route-level — null-check manquant sur `request.user`](#risque-guard-request-user-null) -- [Compteurs in-memory ≠ métriques persistées](#risque-compteurs-inmemory) -- [Interface provider incomplète ou divergente de ses implémentations](#risque-interface-provider-incomplete) -- [Boucle `upsert` N+1 sur synchronisation provider](#risque-upsert-n-plus-un-provider) -- [Stripe `list()` sans gestion de `has_more`](#risque-stripe-list-has-more) -- [Concurrence entre activation locale et webhook sur transition trial → payant](#risque-trial-payant-concurrence) -- [`jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma](#risque-jest-clearallmocks-imbrique) -- [Suppression du cookie après révocation DB sur logout](#risque-cookie-apres-revocation-db) -- [Repository layer non branché (dead layer)](#risque-repository-dead-layer) -- [NestJS 11 — `TooManyRequestsException` inexistante](#risque-nestjs-toomanyrequest) -- [`ForbiddenException` utilisé pour des erreurs de validation](#risque-forbidden-pour-validation) -- [PrismaService — getter explicite manquant sur nouveau modèle](#risque-prismaservice-getter-manquant) -- [Endpoints GET sans contrôle d'accès sur ressource protégée](#risque-get-sans-controle-acces) -- [Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)](#risque-schema-divergence-spec-story) -- [Prisma initialisé au chargement de module — casse le build Next.js](#risque-prisma-init-module-build) -- [`server-only` dans les repositories — bloque les tests unitaires](#risque-server-only-repositories-tests) -- [Controller NestJS corrompu par insertions multiples](#risque-controller-corrompu-insertions) -- [TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)](#risque-ttl-redis-heure-locale) -- [Story "completed" avec tâches ❌ auto-déclarées](#risque-story-completed-taches-echec) -- [Story "done" sans aucun fichier source dans la File List](#risque-story-done-file-list-vide) -- [Prisma `$transaction` : fenêtres TOCTOU (check hors transaction)](#risque-prisma-transaction-toctou-tenantid) -- [Contracts : schema orphelin / type de retour désynchronisé](#risque-contracts-schema-orphelin) -- [Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système](#risque-prisma-or-tenantid-null) -- [Calcul de `nextOrder` hors transaction (race condition `sortOrder`)](#risque-nextorder-hors-transaction) -- [Redirect vers la page désactivée elle-même (boucle infinie feature flags)](#risque-redirect-boucle-infinie) -- [Champ `tenantId` sans FK ni relation Prisma vers `Tenant`](#risque-tenantid-sans-fk-relation) -- [NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert](#risque-adminroleguard-sans-decorateur) -- [Code d’erreur générique sur 409 (conflict)](#risque-code-erreur-generique-409) -- [Tests e2e d’autorisation avec buildApp isolé](#risque-e2e-autorisation-buildapp-isole) - ---- - - -## AuthN/AuthZ dispersée (contrôles d’accès au fil de l’eau) - -### Risques - -- Règles de permissions incohérentes selon endpoints -- Failles “oubliées” sur un endpoint secondaire -- Audit impossible - -### Symptômes - -- Utilisateurs qui accèdent à des ressources non prévues -- Correctifs en urgence “on ajoute un if ici” -- Bugs qui réapparaissent après refactor - -### Bonnes pratiques / mitigations - -- Centraliser authn/authz (middleware/policies) -- Tests sur règles critiques -- Logs/audit des décisions d’accès - ---- - - -## Guard global manquant (request.user jamais peuplé) - -### Risques - -- Chaîne auth bâtie sur une fondation inopérante (tout “a l’air OK” en dev/tests, mais casse en prod) -- Guards aval qui dépendent de `request.user` en erreur (ou contournements involontaires) -- Découvert tard (souvent uniquement en code review ou en prod) - -### Symptômes - -- `request.user` vaut `undefined` dans un guard supposé “après auth” -- Endpoints qui passent alors qu’ils devraient être refusés (si les guards aval se désactivent/retournent true par défaut) -- Tests “verts” car trop mockés (pas de test e2e qui valide le pipeline complet) - -### Bonnes pratiques / mitigations - -- Poser explicitement le guard global dès les foundations (au moins `AuthGuard`) -- Vérifier l’ordre des `APP_GUARD` (AuthGuard avant tout guard qui lit `request.user`) -- Ajouter au minimum 1 test d’intégration/e2e qui prouve que `request.user` est bien peuplé sur un endpoint protégé - ---- - - -## Duplication silencieuse de constantes partagées (contracts) via fichier orphelin - -### Risques - -- Deux sources de vérité qui divergent silencieusement (ex : topics officiels, enums métier, slugs) -- Bug non détecté par TypeScript si la duplication est dans un fichier non importé (code mort) - -### Symptômes - -- Incohérences entre API et client sur des listes/enums “censées être partagées” -- “Ça marche chez moi” selon l’endroit où la constante est importée -- Un fichier de config existe dans `apps/*` mais n’est jamais importé/greffé au runtime - -### Bonnes pratiques / mitigations - -- Toute constante partagée vit dans `packages/contracts/src/` et est importée depuis là (jamais recopiée dans `apps/*`) -- En review : repérer les fichiers “config/constants” ajoutés dans `apps/*` sur des domaines déjà couverts par `contracts` -- (Optionnel) Outillage : intégrer une étape de détection de code mort / exports inutilisés au CI si ça devient récurrent - ---- - - -## Contrats API implicites (validation faible ou absente) - -### Risques - -- Entrées non validées → erreurs bizarres / vulnérabilités -- Changements qui cassent le front et les intégrations - -### Symptômes - -- 500 sur erreurs utilisateur -- Incohérences de format de réponse -- “Ça marche en staging, pas en prod” (données réelles) - -### Bonnes pratiques / mitigations - -- Schémas (OpenAPI/JSON Schema) + validation serveur -- Formats de réponse cohérents -- Versionner/éviter breaking changes - ---- - - -## Erreurs non standardisées (4xx/5xx incohérents) - -### Risques - -- Front et automatisations impossibles à rendre robustes -- Debug long (pas de codes internes, pas de corrélation) - -### Symptômes - -- Clients qui “retry” sur des 4xx -- Messages techniques exposés aux utilisateurs -- Logs inexploitables - -### Bonnes pratiques / mitigations - -- Mapping HTTP standard + format d’erreur stable -- Codes internes d’erreurs applicatives -- requestId/traceId partout - ---- - - -## Migrations risquées / non reproductibles - -### Risques - -- Downtime -- Perte de données -- Incohérence entre environnements - -### Symptômes - -- “Ça marche en local” mais pas en prod -- Migration qui échoue à mi-chemin -- Rollback impossible - -### Bonnes pratiques / mitigations - -- Migrations versionnées + tests staging -- Stratégie expand/contract si besoin -- Plan de rollback/mitigation - ---- - - -## Non-idempotence sur opérations sensibles - -### Risques - -- Doubles paiements / doubles créations -- Webhooks rejoués qui cassent l’état - -### Symptômes - -- Doublons de lignes en DB -- Actions exécutées 2 fois après timeout/retry -- Incidents difficiles à reproduire - -### Bonnes pratiques / mitigations - -- Idempotency key sur endpoints critiques -- Protection anti-doublon côté DB (contraintes uniques) -- Comportement défini en cas de retry - ---- - - -## Stripe (v17+) : confusion `billing_cycle_anchor` vs `current_period_end` - -### Risques - -- Stocker une date de fin de période incorrecte en DB (bug silencieux) -- État d’abonnement incohérent (UI, relances, accès premium) - -### Symptômes - -- `currentPeriodEnd` correspond à une date “bizarre” (souvent proche de la création), ou à un jour du mois -- Des accès premium expirent trop tôt / trop tard - -### Bonnes pratiques / mitigations - -- Ne jamais interpréter `billing_cycle_anchor` comme une date de fin de période -- Utiliser `subscription.current_period_end` (timestamp) pour la fin de période courante -- Ajouter un test sur un événement webhook/Subscription qui vérifie la date persistée - ---- - - -## PostgreSQL / Prisma : `@unique` sur champ nullable (idempotence cassée) - -### Risques - -- Doublons en base malgré un “unique” attendu (PostgreSQL autorise plusieurs `NULL` dans un index UNIQUE) -- Upserts non idempotents si la clé peut être `null` (`where: { externalId: null }` crée plusieurs lignes) - -### Symptômes - -- Plusieurs enregistrements “équivalents” avec `externalId = NULL` -- Rejouer un webhook / retry réseau crée une nouvelle ligne au lieu d’upsert - -### Bonnes pratiques / mitigations - -- Toute clé utilisée dans un `where` d’`upsert` doit être **non-nullable** -- Si un identifiant externe peut légitimement être `null`, ne pas l’utiliser comme clé d’idempotence : choisir une autre clé unique non-nullable - ---- - - -## Observabilité insuffisante (logs non structurés, pas de corrélation) - -### Risques - -- MTTR très élevé : on devine -- Incapacité à mesurer l’impact utilisateur - -### Symptômes - -- Logs “ça a crash” sans contexte -- Impossible de relier une requête à une erreur -- Latence qui dérive sans alerte - -### Bonnes pratiques / mitigations - -- Logs structurés + requestId/traceId -- Métriques de base (latence, erreurs, throughput) -- Alertes simples sur 5xx/latence - ---- - - -## Webhooks entrants — répondre 200 pendant `processing` (event perdu) - -### Risques - -- Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué -- Event non appliqué mais marqué "traité" → état incohérent silencieux - -### Symptômes - -- Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour -- Aucun retry du provider → impossible à détecter sans monitoring actif - -### Bonnes pratiques / mitigations - -- Lock DB (`WebhookEvent`) avec machine d'état : `pending` → `processing` → `processed` / `failed` -- Si `processing` détecté (concurrent) : attendre brièvement la transition `processed`, sinon répondre **non-2xx** (force retry provider) -- Ne jamais passer à `processed` sans preuve d'un traitement effectif -- Contexte technique : Stripe / NestJS — 09-03-2026 - ---- - - -## Redis — thrash de connexion sous charge - -### Risques - -- Connexions concurrentes multiples si `connect()` est appelé "à la demande" sans lock -- Spam logs + saturation connexions quand Redis est down ou lent - -### Symptômes - -- N appels simultanés → N tentatives de connexion en parallèle -- Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis - -### Bonnes pratiques / mitigations - -```typescript -// Pattern single-flight + cooldown + fallback DB best-effort -if (!this.connectPromise) { - this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; }); -} -await this.connectPromise; -// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB -``` - -- Contexte technique : Redis / NestJS — 09-03-2026 - ---- - - -## Entitlements — TTL cache supérieur au SLA de propagation - -### Risques - -- TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti) -- Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel - -### Symptômes - -- Accès premium encore actif après annulation (ou inversement) -- NFR "propagation ≤ 60s" non respecté en cas de webhook manqué - -### Bonnes pratiques / mitigations - -- TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max) -- Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre) -- Contexte technique : Redis / entitlements / NestJS — 09-03-2026 - ---- - - -## Guard NestJS route-level — null-check manquant sur `request.user` - -### Risques - -- Un guard route-level qui lit `request.user.userId` sans null-check lève une `TypeError` (500) si `request.user` est absent -- Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état - -### Symptômes - -- `TypeError: Cannot read properties of undefined (reading 'userId')` en prod -- Tests "verts" car `request.user` mocké globalement, mais pas le guard isolé - -### Bonnes pratiques / mitigations - -```typescript -const user = (request as any).user as { userId: string } | undefined; -if (!user?.userId) { - throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } }); -} -``` - -- **Règle** : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes. -- Contexte technique : NestJS v10+ — 09-03-2026 - ---- - - -## Compteurs in-memory ≠ métriques persistées - -### Risques - -- Compteurs in-memory remis à zéro au restart (perte de données) -- Non agrégables sur plusieurs instances (données partielles par pod) - -### Symptômes - -- Métriques qui "repartent de 0" à chaque déploiement -- Dashboards incorrects en environnement multi-instance - -### Bonnes pratiques / mitigations - -- V1 low-cost : `Redis INCRBY` best-effort par `eventType` → persisté et agrégé multi-instances -- Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ) -- Contexte technique : Redis / NestJS — 09-03-2026 - ---- - - -## Interface provider incomplète ou divergente de ses implémentations - -### Risques - -- Une implémentation expose des méthodes non déclarées dans le contrat commun -- Les appelants contournent l’interface et se couplent à un provider concret -- Une stratégie provider devient non interchangeable en pratique - -### Symptômes - -- Appels avec cast ou accès direct à une implémentation spécifique -- Méthodes présentes dans une classe mais absentes de l’interface -- Régression lors d’un changement de provider - -### Bonnes pratiques / mitigations - -- Toute capacité commune attendue par les appelants doit être déclarée dans l’interface -- Interdire les méthodes “cachées” consommées hors contrat -- Tester au moins une implémentation par le contrat abstrait -- Contexte technique : TypeScript / provider strategy — 10-03-2026 - ---- - - -## Boucle `upsert` N+1 sur synchronisation provider - -### Risques - -- Latence multipliée par le nombre d’items -- Charge DB inutile -- Timeouts ou contention sur gros volumes - -### Symptômes - -- Une boucle applicative exécute un `upsert` par item -- Temps de traitement qui explose avec le volume -- Logs SQL répétitifs et séquentiels - -### Bonnes pratiques / mitigations - -- Batcher quand c’est possible -- Précharger les données nécessaires avant boucle -- Mesurer explicitement le coût d’un `upsert` unitaire dans les flux de sync -- Contexte technique : Prisma / synchronisation provider — 10-03-2026 - ---- - - -## Stripe `list()` sans gestion de `has_more` - -### Risques - -- Pagination tronquée silencieusement -- Réconciliation incomplète d’abonnements, achats ou moyens de paiement -- Décisions métier prises sur un jeu de données partiel - -### Symptômes - -- Comportement correct sur petits comptes mais faux sur comptes plus chargés -- Premiers éléments traités, les suivants ignorés -- Absence de boucle de pagination ou d’auto-pagination - -### Bonnes pratiques / mitigations - -- Traiter explicitement `has_more` -- Utiliser l’auto-pagination Stripe si adaptée -- Tester au moins un cas avec plusieurs pages de résultats -- Contexte technique : Stripe API — 10-03-2026 - ---- - - -## Concurrence entre activation locale et webhook sur transition trial → payant - -### Risques - -- Double création ou double attachement d’une ressource unique -- Conflit `P2002` -- État local différent de l’état Stripe pendant la transition - -### Symptômes - -- La transition fonctionne parfois, puis échoue aléatoirement -- Un webhook Stripe et une action applicative écrivent la même mutation métier -- Erreurs d’unicité lors de l’activation payante - -### Bonnes pratiques / mitigations - -- Définir une seule source autorisée pour chaque transition d’état -- Rendre les écritures idempotentes -- Sérialiser ou réconcilier explicitement les transitions pilotées à la fois par action utilisateur et webhook -- Contexte technique : Stripe / Prisma / trial subscription — 10-03-2026 - ---- - - -## `jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma - -### Risques - -- Remise à zéro d’un setup attendu par un scope de test plus profond -- Tests verts ou rouges pour de mauvaises raisons -- Forte difficulté à comprendre l’état réel des mocks - -### Symptômes - -- Comportement différent selon l’ordre ou le niveau d’imbrication des `describe` -- Mocks Prisma “perdus” entre deux tests -- Corrections locales qui cassent d’autres blocs de tests - -### Bonnes pratiques / mitigations - -- Centraliser la stratégie de reset des mocks -- Éviter les `clearAllMocks()` concurrents à plusieurs niveaux de nesting -- Préférer un setup explicite et local par scénario quand les mocks Prisma sont structurants -- Contexte technique : Jest / Prisma / tests NestJS — 10-03-2026 - ---- - - -## Suppression du cookie après révocation DB sur logout - -### Risques - -- Si la révocation DB échoue avant la suppression du cookie, l’utilisateur garde un cookie local devenu incohérent -- L’utilisateur peut rester bloqué dans un état où il ne peut plus se déconnecter proprement -- Le comportement diffère selon la disponibilité de la base - -### Symptômes - -- Logout qui échoue par intermittence quand la DB est instable -- Cookie de session toujours présent côté navigateur après erreur serveur -- Réessais de logout qui produisent des états difficiles à diagnostiquer - -### Bonnes pratiques / mitigations - -- Toujours supprimer le cookie en premier, même si la révocation DB échoue ensuite -- Traiter la suppression côté DB en best-effort ou avec gestion d’idempotence adaptée -- Vérifier en test qu’un échec DB ne laisse pas l’accès browser actif -- Contexte technique : Next.js / auth par cookie / session persistée — 16-03-2026 - ---- - - -## Repository layer non branché (dead layer) - -### Risques - -- Donner une impression de sécurité alors que le code métier continue d’appeler l’ORM directement -- Multiplier les chemins d’accès aux données avec des règles différentes -- Payer le coût d’une abstraction qui n’a aucun effet réel - -### Symptômes - -- Un repository est créé mais les anciens call sites Prisma restent en place -- Les nouvelles règles de scoping ou de sécurité ne s’appliquent pas partout -- La review montre des fichiers de repository peu ou jamais importés - -### Bonnes pratiques / mitigations - -- Vérifier qu’une nouvelle couche d’abstraction est réellement branchée dans les call sites existants -- Rechercher explicitement les appels directs restants lors de la review -- Refuser l’introduction d’une couche repository tant que la migration effective n’est pas faite -- Contexte technique : TypeScript / Prisma / refactor d’accès aux données — 16-03-2026 - ---- - - -## NestJS 11 — `TooManyRequestsException` inexistante - -### Risques - -- `TooManyRequestsException` n’est pas exportée par `@nestjs/common` en NestJS ≥ 11 -- Erreur de compilation ou 500 si utilisée directement - -### Symptômes - -- `Cannot find name ‘TooManyRequestsException’` à la compilation -- Test qui passe sur NestJS 10 mais échoue sur 11+ - -### Bonnes pratiques / mitigations - -```typescript -// Pattern sûr pour HTTP 429 -throw new HttpException( - { error: { code: ‘QUOTA_EXCEEDED’, message: ‘...’ } }, - HttpStatus.TOO_MANY_REQUESTS, -); -``` - -- Contexte technique : NestJS v11+ — 20-03-2026 - ---- - - -## `ForbiddenException` (403) utilisé pour des erreurs de validation - -### Risques - -- Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403 -- Sémantique API incorrecte → comportements clients imprévisibles - -### Symptômes - -- `ForbiddenException` lancée pour des tags invalides, des formats incorrects, des liens HTTP -- Clients API qui ignorent ces erreurs ou les traitent comme des refus d’accès - -### Bonnes pratiques / mitigations - -Tableau de correspondance : - -| Cas | Exception correcte | Code HTTP | -|---|---|---| -| Tags invalides, contenu trop long, format incorrect | `BadRequestException` | 400 | -| Accès refusé explicitement (accès forum, trial read-only) | `ForbiddenException` | 403 | -| Quota dépassé | `HttpException(429)` via `HttpStatus.TOO_MANY_REQUESTS` | 429 | - -- **Règle** : HTTP 403 = "tu n’as pas le droit d’effectuer cette action". HTTP 400 = "ta requête est mal formée". -- Contexte technique : NestJS / HTTP — 20-03-2026 - ---- - - -## PrismaService — getter explicite manquant sur nouveau modèle - -### Risques - -- L’ajout d’un modèle dans `schema.prisma` sans son getter dans `PrismaService` casse le typecheck -- Erreur silencieuse si les modules sont peu typés - -### Symptômes - -- `Property ‘forum’ does not exist on type ‘PrismaService’` à la compilation -- Module fonctionnel sur le `PrismaClient` direct mais cassé via `PrismaService` - -### Bonnes pratiques / mitigations - -Tout ajout de modèle Prisma = **deux actions** : - -1. Ajouter le modèle dans `schema.prisma` -2. Ajouter le getter dans `prisma.service.ts` - -```typescript -// apps/api/src/infra/prisma/prisma.service.ts -get forum() { - return this.client.forum; -} -``` - -- **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour. -- Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026 - ---- - - -## Endpoints GET sans contrôle d'accès sur ressource protégée - -### Risques - -- Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié -- La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses - -### Symptômes - -- `getCategories`, `getThreads` ou équivalent accessible sans vérification d'entitlements -- Endpoint write protégé par `assertForumAccess` mais GET correspondant non protégé - -### Bonnes pratiques / mitigations - -- Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les GET -- **Checklist review** : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé - -- Contexte technique : NestJS / app-alexandrie — 23-03-2026 - ---- - - -## Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent) - -### Risques - -- Une tâche de story cochée ✅ implique un champ (ex: `consumedAt`, `tokenHash`) qui n'existe pas dans `schema.prisma` -- Le code compile ou passe en review sans que le champ soit réellement présent en DB - -### Symptômes - -- Erreur à l'exécution sur un champ inexistant malgré une story marquée "done" -- `schema.prisma` ne contient pas le champ mentionné dans les tâches - -### Bonnes pratiques / mitigations - -- Avant de marquer une tâche ✅, croiser avec `schema.prisma` pour confirmer que le champ existe réellement -- Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier - -- Contexte technique : Prisma / app-template-resto — 16-03-2026 - ---- - - -## Prisma initialisé au chargement de module — casse le build Next.js - -### Risques - -- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build - -### Symptômes - -- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build` -- L'app tourne en dev mais le build CI échoue - -### Bonnes pratiques / mitigations - -- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier -- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB -- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js - -- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026 - ---- - - -## `server-only` dans les repositories — bloque les tests unitaires - -### Risques - -- `import "server-only"` empêche l'exécution des fichiers hors runtime Next.js -- Les tests Node.js échouent avec `Error: This module cannot be imported from a Client Component module` - -### Symptômes - -- Tests qui passent via le dev server mais échouent via `jest` en mode node -- Erreur au `require()` d'un repository depuis un test unitaire - -### Bonnes pratiques / mitigations - -- Ne mettre `server-only` que dans les fichiers qui utilisent des APIs Next.js runtime (`cookies()`, `headers()`, `redirect()`) -- **Ne pas** mettre `server-only` dans les repositories purs (qui n'appellent que Prisma) -- Alternative de secours : créer un stub `node_modules/server-only/index.js` no-op pour les tests - -- Contexte technique : Next.js App Router / Jest — app-template-resto 16-03-2026 - ---- - - -## Controller NestJS corrompu par insertions multiples - -### Risques - -- Des méthodes imbriquées, décorateurs orphelins ou routes dupliquées cassent la syntaxe TypeScript sans que le compilateur ne l'attrape toujours -- La story est marquée "completed" alors que le code ne compile pas - -### Symptômes - -- `@Get('/route')` apparaît dans le corps d'une autre méthode -- La même route est déclarée 2-3 fois dans le même controller -- Erreur NestJS au runtime mais pas à la compilation - -### Bonnes pratiques / mitigations - -- Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original -- Ne jamais insérer par blocs séparés — la concaténation casse la structure AST -- **Checklist review** : grep `@Get\|@Post\|@Patch\|@Delete` dans le controller et vérifier qu'aucune route n'est dupliquée - -- Contexte technique : NestJS / TypeScript — app-alexandrie 20-03-2026 - ---- - - -## TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h) - -### Risques - -- Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC - -### Symptômes - -- Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement -- Comportement différent en dev local (TZ machine) et en prod (TZ container) - -### Bonnes pratiques / mitigations - -```typescript -// ✅ CORRECT — UTC midnight garanti -const midnight = new Date( - Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1), -); -const ttlMs = midnight.getTime() - now.getTime(); - -// ❌ RISQUÉ — heure locale du serveur -const endOfDay = new Date(); -endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur -``` - -- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review - -- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026 - ---- - - -## Story "completed" avec tâches ❌ auto-déclarées - -### Risques - -- Un agent sette `Status: completed` alors que son propre Dev Agent Record liste des items ❌ non implémentés -- Le store mobile, service ou tests peuvent être déclarés manquants par l'agent lui-même mais la story semble terminée - -### Symptômes - -- Dev Agent Record contient `❌ store mobile non implémenté` mais `Status: completed` -- Code review découvre des ACs non satisfaits - -### Bonnes pratiques / mitigations - -- Avant de setter `Status: completed`, vérifier que le Dev Agent Record ne contient aucun ❌ -- En cas de doute ou d'item manquant, setter `Status: review` pour déclencher la code review -- **Règle** : `Status: completed` = zéro ❌ auto-déclaré dans le Dev Agent Record - -- Contexte technique : BMAD / workflow agent — app-alexandrie 20-03-2026 - ---- - - -## Story "done" sans aucun fichier source dans la File List - -### Risques - -- Un agent peut halluciner la completion d'une story en produisant une note générique sans écrire de code -- La File List ne contient que des fichiers `_bmad-output/` mais aucun `src/`, `prisma/`, `tests/` - -### Symptômes - -- Completion note générique du type "Ultimate context engine analysis completed" -- File List réduite à 2 fichiers meta (story file, sprint-status) -- `git log --follow src/` ne montre aucun commit lié à la story - -### Bonnes pratiques / mitigations - -- Lors d'une code review, si la File List ne contient aucun fichier source : traiter comme non implémentée -- Vérifier avec `git log --follow src/` avant d'accepter le `Status: done` -- Ne pas faire confiance au status `done` sans preuve dans le code - -- Contexte technique : BMAD / agent Codex — app-template-resto 21-03-2026 - ---- - - -## Prisma `$transaction` : fenêtres TOCTOU (check hors transaction) - -### Risques - -- Un pre-check + une `$transaction` avec un `update` non sécurisé crée une fenêtre TOCTOU -- Deux appels concurrents peuvent tous deux passer le check et agir simultanément -- En multi-tenant : un bug upstream peut permettre une écriture cross-tenant malgré le guard applicatif - -### Symptômes - -- Double action sur un état booléen (ex : double mise en vitrine) si le check n'est pas dans la transaction -- Écriture sur une ressource d'un autre tenant possible en race condition - -### Bonnes pratiques / mitigations - -**Cas 1 — Multi-tenant : inclure `tenantId` dans chaque écriture** - -```typescript -// ❌ Anti-pattern — check OK mais écriture sans tenantId -const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } }); -await prisma.$transaction( - ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } })) -); - -// ✅ Défense en profondeur — tenantId dans chaque écriture -await prisma.$transaction( - ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } })) -); -``` - -- Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure `tenantId` dans le WHERE, même dans une transaction précédée d'un check -- Utiliser `updateMany`/`deleteMany` pour inclure `tenantId` sans exception si 0 lignes - -**Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction** - -```typescript -// ❌ Anti-pattern : check d'état hors transaction -if (resource.isActive) throw ...; -await prisma.$transaction(async (tx) => { - // resource.isActive a pu changer entre-temps - return tx.resource.update(...); -}); - -// ✅ Pattern correct : check ET update dans la transaction -await prisma.$transaction(async (tx) => { - const current = await tx.resource.findUnique({ where: { id } }); - if (current?.isActive) throw ...; // re-check atomique - const count = await tx.resource.count(...); - if (count >= LIMIT) throw ...; - return tx.resource.update(...); -}); -``` - -- Règle : tout guard métier de type "déjà fait / plafond atteint" doit être vérifié à l'intérieur de la transaction, pas avant - -- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 ; NestJS / Prisma — app-alexandrie 23-03-2026 - ---- - - -## Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système - -### Risques - -- Sur un modèle à `tenantId` nullable distinguant ressources "système" et "tenant", un filtre `{ isSystem: true }` sans `tenantId: null` expose des ressources corrompues à tous les tenants - -### Symptômes - -- Un tag `isSystem: true` avec `tenantId` non-null est exposé à tous les tenants -- Bug de sécurité difficile à détecter car le comportement nominal semble correct - -### Bonnes pratiques / mitigations - -```typescript -// ❌ Trop permissif -OR: [{ isSystem: true }, { tenantId, isSystem: false }] - -// ✅ Défense en profondeur — double condition sur la branche système -OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }] -``` - -- Règle : sur tout modèle `tenantId?` (nullable) + flag `isSystem`/`isGlobal`/`isPublic`, la branche "ressource publique" du filtre OR doit toujours inclure `tenantId: null` - -- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 - ---- - - -## Calcul de `nextOrder` hors transaction (race condition `sortOrder`) - -### Risques - -- Deux requêtes concurrentes obtiennent le même `MAX(sortOrder)` et créent deux entités avec le même `sortOrder` - -### Symptômes - -- Deux items avec le même `sortOrder` dans la même catégorie/scope -- Bug aléatoire selon la charge — invisible en dev, présent en prod - -### Bonnes pratiques / mitigations - -```typescript -// ✅ Calcul dans la transaction interactive -return prisma.$transaction(async (tx) => { - const maxOrder = await tx.entity.aggregate({ - where: { tenantId, scopeId }, - _max: { sortOrder: true }, - }); - const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1; - return tx.entity.create({ data: { ..., sortOrder: nextOrder } }); -}); -``` - -- Règle : ne jamais calculer `maxOrder` hors de la transaction qui crée l'entité - -- Contexte technique : Prisma / transactions — app-template-resto 21-03-2026 - ---- - - -## Redirect vers la page désactivée elle-même (boucle infinie feature flags) - -### Risques - -- Une page désactivée redirige vers elle-même via le fallback — boucle infinie silencieuse absorbée par Next.js mais UX cassée - -### Symptômes - -- Page `/` désactivée → redirect vers `buildLocalizedPath("home")` = `/` → boucle -- Next.js absorbe la boucle mais l'utilisateur voit un écran bloqué ou vide - -### Bonnes pratiques / mitigations - -```typescript -// Si la page est sa propre destination de fallback, ne pas rediriger -if (pageKey === "home") return null; // évite redirect home → home -return buildLocalizedPath(locale, "home"); -``` - -- Règle : dans tout mécanisme de redirection sur page désactivée, toujours vérifier que `pageKey !== fallbackKey` -- Retourner `null` (accès non bloqué) plutôt que de boucler - -- Contexte technique : Next.js App Router / feature flags tenant — app-template-resto 17-03-2026 - ---- - - -## Champ `tenantId` sans FK ni relation Prisma vers `Tenant` - -### Risques - -- Un `tenantId TEXT NOT NULL` sans relation Prisma ne génère aucune FK en DB -- L'isolation multi-tenant n'est pas enforced au niveau base de données - -### Symptômes - -- Migration SQL sans `ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"` -- Prisma ne génère pas de FK automatiquement sans `@relation` déclarée - -### Bonnes pratiques / mitigations - -Tout modèle tenant-scoped doit avoir les trois : -1. `tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)` dans le modèle Prisma -2. La relation inverse dans `Tenant` (ex: `menuCategories MenuCategory[]`) -3. La FK correspondante dans la migration SQL - -- **Checklist review** : vérifier systématiquement que les nouveaux modèles respectent ce guardrail - -- Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026 - ---- - - -## NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert - -### Risques - -- `AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()` -- Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer sans vérification - -### Symptômes - -- Endpoint admin accessible à tout utilisateur authentifié -- Zéro erreur de compilation ou de démarrage — le bug est silencieux - -### Bonnes pratiques / mitigations - -```typescript -// ✅ Correct — les deux décorateurs ensemble -@Post('admin/ressource') -@UseGuards(AdminRoleGuard) -@RequireAdminRole() -async createRessource(...) {} - -// ❌ Silencieusement non protégé — @RequireAdminRole() manquant -@Post('admin/ressource') -@UseGuards(AdminRoleGuard) -async createRessource(...) {} -``` - -- Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur -- **Checklist review** : vérifier systématiquement les endpoints admin que `@RequireAdminRole()` est présent - -- Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026 - ---- - - -## Contracts : schema orphelin / type de retour désynchronisé - -### Risques - -- Un `RequestSchema` défini dans `packages/contracts` mais jamais importé dans le controller ni le service mobile → dead code silencieux qui crée une fausse confiance -- Un type de retour inline (`string` brut) à la place du type contracts → désynchronisation silencieuse entre contrat et implémentation - -### Symptômes - -- `grep` du nom du schema ne trouve aucun `import` en dehors de sa définition -- Service retourne `Promise<{ status: string }>` au lieu de `Promise` — le `status` n'est pas validé comme `CurationStatus` -- Endpoints `POST /action` sans body ayant un schema `{ pathParam: string }` — le param vient du path, pas du body - -### Bonnes pratiques / mitigations - -À chaque story qui ajoute des schemas dans `packages/contracts`, vérifier en review : - -1. Chaque `RequestSchema` est utilisé dans un `ZodValidationPipe` (API) ou importé dans le service mobile. -2. Les `ResponseSchema` correspondent au type de retour typé du service (`Promise`, pas un type inline). -3. Les endpoints sans body (`POST /action`) définissent `z.object({})` ou omettent le body schema — ne jamais placer les path params dans le body schema. - -```typescript -// ❌ Anti-pattern — type inline, status non typé -async showcaseThread(...): Promise<{ threadId: string; status: string }> { ... } - -// ✅ Pattern correct — type contracts importé -import type { CurationResponse } from '@app-alexandrie/contracts'; -async showcaseThread(...): Promise { ... } -``` - -- Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026 - ---- - - -## Code d’erreur générique sur 409 (conflict) - -### Risques - -- Erreurs indistinguables côté client et monitoring -- Tests/automatisations incapables de réagir à des cas métier distincts - -### Symptômes - -- Utilisation de codes génériques (ex: `VALIDATION_ERROR`, `INTERNAL_ERROR`) pour des 409 CONFLICT -- Impossibilité de distinguer “alias déjà pris” vs “autre conflit métier” côté client - -### Bonnes pratiques / mitigations - -- 1 scénario métier distinct = 1 code d’erreur dédié (ex: `ALIAS_ALREADY_RESOLVED`, `HANDLE_ALREADY_TAKEN`) -- Centraliser les codes dans `error-code.ts` et les mapper systématiquement - ---- - - -## Tests e2e d’autorisation avec buildApp isolé - -### Risques - -- Scénarios non‑abonné / droits inactifs impossibles à tester si le `buildApp` partagé active des entitlements en `beforeAll` -- Pollution croisée des tests e2e par partage d’instance - -### Symptômes - -- Impossible de reproduire un 403 “non abonné” dans un `describe` qui mocke des droits actifs globalement - -### Bonnes pratiques / mitigations - -- Créer une instance isolée pour les scénarios alternatifs: - -```ts -const app = await buildApp({ - getEntitlementsForUser: jest.fn().mockResolvedValue({ subscription: { isActive: false, ... } }) -}); -// ... test ... -await app.close(); -``` - -- Ne pas surcharger un mock global partagé; préférer un `buildApp` dédié par scénario diff --git a/10_frontend_patterns_valides.md b/10_frontend_patterns_valides.md deleted file mode 100644 index c6cda56..0000000 --- a/10_frontend_patterns_valides.md +++ /dev/null @@ -1,830 +0,0 @@ -# Patterns front-end validés - -Ce fichier contient **uniquement** des patterns front-end : - -- testés, -- validés, -- utilisés dans des projets réels (ou des apps complètes, pas des snippets isolés). - -Il sert de **mémoire durable** pour éviter : - -- de refaire les mêmes erreurs, -- de redélibérer éternellement sur des sujets déjà tranchés, -- de propager des “bonnes pratiques” théoriques non éprouvées. - -Dernière mise à jour : 23-03-2026 - ---- - -## Index - -- [Gestion explicite des états UI (loading / empty / error)](#pattern-etats-ui-loading-empty-error) -- [Séparation claire server state / client state](#pattern-separation-server-state-client-state) -- [Formulaire robuste avec validation et erreurs explicites](#pattern-formulaire-robuste) -- [Navigation réactive post-action async (React / Expo Router)](#pattern-navigation-reactive-post-action-async) -- [Refresh idempotent sur store de liste paginée](#pattern-refresh-idempotent-liste-paginee) -- [UI admin légère sur domaine existant](#pattern-ui-admin-legere-domaine-existant) -- [Intégration tierce en mode link-out — préférer une page locale canonique](#pattern-link-out-page-locale-canonique) -- [Design Tokens natifs TypeScript (Expo / React Native)](#pattern-design-tokens-expo-rn) -- [Tests de styles React Native sans renderer JSX](#pattern-tests-styles-sans-renderer) -- [Export des styles de composant pour réutilisation partielle](#pattern-export-styles-composant) -- [Token typography par usage sémantique (React Native)](#pattern-token-typography-semantique) -- [Click-to-load strict pour les embeds tiers (iframe/widget)](#pattern-click-to-load-embeds-tiers) -- [Toggle optimiste avec rollback (React Server Action)](#pattern-toggle-optimiste-rollback) -- [Server Action retournant l'entité — élimination de `router.refresh()` sur create/edit](#pattern-server-action-retourne-entite) -- [ESLint flat config avec presets Next.js (`eslint.config.mjs`)](#pattern-eslint-flat-config-nextjs) -- [Grilles 2 colonnes FR/EN — mobile-first](#pattern-grilles-2-colonnes-mobile-first) - ---- - -## Règle d’or - -Si ce n’est pas **confirmé comme fonctionnel et utile**, -**ça n’a rien à faire ici**. - -- Pas de conseils vagues -- Pas de patterns “à la mode” -- Pas de dépendance implicite à un framework ou une version non précisée - ---- - -## Périmètre couvert - -- SPA et webapps -- UX technique (forms, erreurs, loading, feedback) -- State management (client / server) -- Architecture front-end -- Performance et accessibilité -- Sécurité front (au niveau applicatif) -- DX et maintenabilité - -Ce fichier traite le **front-end comme un logiciel en production**, -au même niveau d’exigence que le backend. - ---- - -## Format standard d’un pattern (obligatoire) - -## Pattern : - -- Objectif : ce que le pattern résout -- Contexte : type d’application / contraintes -- Quand l’utiliser : cas pertinents -- Quand l’éviter : contre-exemples -- Avantages : bénéfices concrets -- Limites / vigilance : pièges, dette potentielle -- Validé le : DD-MM-YYYY -- Contexte technique : framework + version + tooling principal - -### Implémentation (exemple minimal) - -```txt -(contenu volontairement minimal, lisible, non-magique) -``` - ---- - - -## Pattern : Gestion explicite des états UI (loading / empty / error) - -### Synthèse - -- **Objectif** : éviter les interfaces ambiguës ou incohérentes en rendant explicites tous les états possibles d’une vue. -- **Contexte** : SPA ou webapp consommant des données asynchrones (API, backend, cache). -- **Quand l’utiliser** : dès qu’une vue dépend de données externes ou d’un traitement async. -- **Quand l’éviter** : vues purement statiques ou synchrones sans dépendance externe. - -### Analyse - -- **Avantages** : - - UX plus prévisible et compréhensible - - Debug facilité (état visible = problème identifiable) - - Base saine pour tests et accessibilité -- **Limites / vigilance** : - - Peut sembler verbeux sur des écrans simples - - Nécessite une discipline pour ne pas “court-circuiter” les états - -### Validation - -- Validé le : 25-01-2026 -- Contexte technique : SPA (React / Vue / Svelte agnostique), API HTTP - -### Implémentation (exemple minimal) - -```txt -if (loading) { - afficher un skeleton ou spinner -} else if (error) { - afficher un message clair + action possible -} else if (data est vide) { - afficher un état empty explicite -} else { - afficher la vue nominale -} -``` - -### Checklist - -- [ ] Aucun écran blanc ou silencieux -- [ ] Message d’erreur compréhensible pour l’utilisateur -- [ ] États testables individuellement -- [ ] Accessibilité respectée (focus, lecture écran) -- [ ] Pas de logique métier cachée dans le rendu - ---- - - -## Pattern : Séparation claire server state / client state - -### Synthèse - -- **Objectif** : éviter le mélange des responsabilités entre données serveur et état local UI. -- **Contexte** : SPA ou webapp consommant une API avec interactions utilisateur. -- **Quand l’utiliser** : dès que l’application affiche des données distantes modifiables ou synchronisées. -- **Quand l’éviter** : applications très simples ou purement statiques. - -### Analyse - -- **Avantages** : - - Logique plus lisible et testable - - Réduction des bugs liés aux états incohérents - - Évolutivité facilitée quand l’app grossit -- **Limites / vigilance** : - - Demande de la rigueur dans le découpage - - Peut sembler abstrait au début pour des petits projets - -### Validation - -- Validé le : 25-01-2026 -- Contexte technique : SPA agnostique (React / Vue / Svelte), API HTTP - -### Implémentation (exemple minimal) - -```txt -serverState = données venant du backend (fetch, cache, sync) -clientState = état local UI (filtres, onglets, modales, formulaires) - -Ne jamais : -- stocker du state UI dans le cache serveur -- dériver la logique UI directement des réponses API sans adaptation -``` - -### Checklist - -- [ ] Les données serveur peuvent être invalidées / rechargées -- [ ] L’état UI est local et réinitialisable -- [ ] Les responsabilités sont lisibles dans le code -- [ ] Les tests peuvent cibler chaque type d’état -- [ ] Pas de dépendance implicite entre UI et API - ---- - - -## Pattern : Formulaire robuste avec validation et erreurs explicites - -### Synthèse - -- **Objectif** : garantir des formulaires fiables, compréhensibles et maintenables. -- **Contexte** : toute interface avec saisie utilisateur et règles métier. -- **Quand l’utiliser** : dès qu’un formulaire dépasse un simple champ isolé. -- **Quand l’éviter** : formulaires ultra-simples sans validation réelle. - -### Analyse - -- **Avantages** : - - UX claire (l’utilisateur sait quoi corriger) - - Moins d’erreurs silencieuses - - Base saine pour tests et accessibilité -- **Limites / vigilance** : - - Peut sembler verbeux sans discipline - - Risque de duplication si mal factorisé - -### Validation - -- Validé le : 25-01-2026 -- Contexte technique : Front-end agnostique, API HTTP - -### Implémentation (exemple minimal) - -```txt -- Validation côté client (format, champs requis) -- Validation côté serveur (règles métier) -- Mapping explicite des erreurs serveur → champs UI -- Aucun submit silencieux -``` - -### Checklist - -- [ ] Messages d’erreur compréhensibles et localisés -- [ ] Validation client + serveur cohérente -- [ ] Focus automatique sur le champ en erreur -- [ ] États loading / disabled gérés -- [ ] Tests sur cas valides et invalides - ---- - - -## Pattern : Navigation réactive post-action async (React / Expo Router) - -### Synthèse - -- **Objectif** : déclencher la navigation après une action asynchrone (login, register, submit) de façon idiomatique et sans bypasser la réactivité React. -- **Contexte** : SPA ou app mobile React avec state management (Zustand, Redux, Context) et router déclaratif (React Router, Expo Router, Next.js App Router). -- **Quand l'utiliser** : dès qu'une navigation dépend du résultat d'une action async. -- **Quand l'éviter** : navigations synchrones sans état async impliqué. - -### Analyse - -- **Avantages** : - - Respecte le cycle de vie React (pas de lecture de state hors cycle) - - Re-render automatique si l'état change entre-temps - - Testable : on peut assert sur l'état, pas sur des effets de bord -- **Limites / vigilance** : - - Ne pas oublier les dépendances du `useEffect` (ESLint react-hooks/exhaustive-deps) - - Gérer le cas "composant démonté" si la navigation peut être annulée - -### Validation - -- Validé le : 07-03-2026 -- Contexte technique : React 18+ / Zustand / Expo Router — pattern applicable sur React Router, Next.js App Router - -### Implémentation (exemple minimal) - -```typescript -// ❌ Anti-pattern : lecture de state hors cycle React -const handleSubmit = async () => { - await login(email, password); - const { accessToken } = useAuthStore.getState(); // bypasse la réactivité - if (accessToken) router.replace('/(tabs)'); -}; - -// ✅ Pattern correct : useEffect réactif sur le state -const { accessToken, isLoading, error } = useAuthStore(); - -useEffect(() => { - if (accessToken && !isLoading && !error) { - router.replace('/(tabs)'); - } -}, [accessToken, isLoading, error]); - -const handleSubmit = async () => { - await login(email, password); - // la navigation se déclenche via useEffect quand le store se met à jour -}; -``` - -### Pour les callbacks OAuth (ref nécessaire) - -```typescript -// Quand un callback externe déclenche la navigation -const pendingOAuth = useRef(false); - -useEffect(() => { - if (pendingOAuth.current && accessToken) { - pendingOAuth.current = false; - router.replace('/(tabs)'); - } -}, [accessToken]); - -const handleOAuth = async () => { - pendingOAuth.current = true; - await exchangeWithIdp(token); -}; -``` - -### Checklist - -- [ ] Aucun `store.getState()` utilisé pour lire l'état post-action dans un handler -- [ ] `useEffect` avec dépendances explicites (state pertinent + isLoading + error) -- [ ] Cas d'erreur géré (ne pas naviguer si error est défini) -- [ ] `useRef` si le trigger vient d'un callback externe (OAuth, deep link) -- [ ] Convention documentée dans la story foundations / project-context avant les premiers écrans - ---- - - -## Pattern : Refresh idempotent sur store de liste paginée - -### Synthèse - -- **Objectif** : garantir qu’un pull-to-refresh recharge une liste paginée sans doublons, sans courses réseau et sans état intermédiaire incohérent. -- **Contexte** : app mobile ou SPA avec store de domaine (ex. Zustand) et pagination incrémentale. -- **Quand l’utiliser** : dès qu’une même liste supporte à la fois `loadMore` et `refresh`. -- **Quand l’éviter** : listes purement statiques ou données entièrement remplacées sans pagination. - -### Analyse - -- **Avantages** : - - évite les doublons lors des refresh concurrents - - garde une transition atomique entre ancien et nouvel état - - rend le comportement async testable côté store -- **Limites / vigilance** : - - impose une discipline claire entre `refresh` et `loadMore` - - demande une clé d’identité stable pour dédupliquer les items - -### Validation - -- Validé le : 10-03-2026 -- Contexte technique : React Native / Expo / Zustand / listes paginées - -### Implémentation (exemple minimal) - -```txt -- conserver une promesse de refresh partagée tant qu’un refresh est en vol -- refuser ou réutiliser tout refresh concurrent au lieu d’en lancer un second -- remplacer atomiquement la liste à la fin du refresh -- dédupliquer les items par identifiant au merge des pages suivantes -- empêcher `loadMore` de fusionner sur un snapshot devenu obsolète -``` - -### Checklist - -- [ ] Une seule promesse de refresh en vol à la fois -- [ ] `refresh` et `loadMore` ont des garde-fous explicites -- [ ] La liste est remplacée atomiquement après refresh -- [ ] Les pages suivantes sont dédupliquées par identifiant stable -- [ ] Tests sur refresh concurrent + refresh suivi de pagination - ---- - - -## Pattern : UI admin légère sur domaine existant - -### Synthèse - -- **Objectif** : ajouter une capacité interne simple sans ouvrir trop tôt un back-office séparé ni dupliquer la logique métier. -- **Contexte** : app mobile ou SPA avec un domaine métier déjà structuré et quelques actions internes ponctuelles. -- **Quand l’utiliser** : publication, activation, modération légère, bascule de statut, action opérateur simple. -- **Quand l’éviter** : permissions complexes, workflows multiples, audit riche ou volume d’actions qui justifie un vrai espace d’administration. - -### Analyse - -- **Avantages** : - - réutilise le service et le store métier existants - - limite le coût de structure pour une capacité admin mince - - garde les mutations testables et lisibles -- **Limites / vigilance** : - - ne pas laisser cette approche dériver vers un pseudo back-office implicite - - le refresh après mutation doit être explicite sur les vues impactées - -### Validation - -- Validé le : 10-03-2026 -- Contexte technique : React Native / Expo Router / store de domaine - -### Implémentation (exemple minimal) - -```txt -- ajouter une route dédiée minimale pour l’action interne -- réutiliser le service/store métier existant au lieu de créer une couche parallèle -- afficher le statut courant avant action -- bloquer les actions concurrentes avec un flag explicite (`isUpdating*`) -- déclencher un refresh explicite des vues impactées après succès -- éviter les mutations en fire-and-forget -``` - -### Checklist - -- [ ] Route dédiée minimale, pas de mini back-office générique -- [ ] Réutilisation du domaine métier existant -- [ ] Garde-fou explicite contre les doubles actions -- [ ] Refresh explicite après mutation réussie -- [ ] Tests sur succès, erreur et action concurrente - ---- - ---- - - -## Pattern : Intégration tierce en mode link-out — préférer une page locale canonique - -### Synthèse - -- **Objectif** : éviter les parcours concurrents et centraliser les garde-fous UX quand une fonctionnalité publique dépend d’un service tiers externe. -- **Contexte** : site ou webapp avec CTA publics menant vers un tiers de réservation, paiement, prise de rendez-vous ou formulaire externe. -- **Quand l’utiliser** : dès qu’une fonctionnalité externe dispose d’une page locale dédiée côté produit (`/reservation`, `/booking`, etc.). -- **Quand l’éviter** : si le produit assume volontairement une sortie directe unique vers le tiers, sans page locale intermédiaire ni besoin de contextualisation. - -### Analyse - -- **Avantages** : - - UX plus cohérente entre home, navigation et pages dédiées - - garde-fous, wording et fallbacks centralisés au même endroit - - facilite l’évolution future vers embed, click-to-load ou variantes de parcours -- **Limites / vigilance** : - - ajoute une étape intermédiaire si la page locale n’apporte aucune valeur - - demande de maintenir l’alignement entre les CTA internes et le parcours canonique - -### Validation - -- Validé le : 19-03-2026 -- Contexte technique : site web public / intégration tierce en mode lien externe - -### Implémentation (exemple minimal) - -```txt -- faire pointer les CTA internes (home, nav, landing) vers une page locale dédiée -- faire de cette page locale le point canonique vers le service tiers externe -- centraliser sur cette page le contexte utile, les garde-fous et les fallbacks -- éviter les sorties directes concurrentes vers le tiers depuis plusieurs endroits du site -``` - -### Checklist - -- [ ] Un parcours canonique unique est défini -- [ ] Les CTA internes convergent vers la page locale dédiée -- [ ] Les garde-fous et fallbacks sont centralisés -- [ ] Les sorties directes concurrentes vers le tiers sont évitées ou justifiées - ---- - - -## Pattern : Design Tokens natifs TypeScript (Expo / React Native) - -### Synthèse - -- **Objectif** : centraliser les tokens de design sans librairie externe (NativeBase, Tamagui), typés et barrel-exportés. -- **Contexte** : app Expo / React Native avec un système de design à maintenir. -- **Quand l’utiliser** : dès le début d’un projet mobile, avant les premiers composants. -- **Quand l’éviter** : si une librairie UI opinionée est déjà choisie et gère ses propres tokens. - -### Analyse - -- **Avantages** : - - aucune dépendance externe, zéro configuration magique - - autocomplétion TypeScript exacte via `as const` + types dérivés - - facile à migrer vers un design system plus élaboré ultérieurement -- **Limites / vigilance** : - - les fichiers TTF doivent être présents dans `assets/fonts/` — Google Fonts ne peut pas être téléchargé automatiquement, documenter comme pré-requis dans la story - - ne pas réutiliser les tokens `spacing` pour les dimensions de composants (voir risques) - -### Validation - -- Validé le : 19-03-2026 -- Contexte technique : Expo SDK 52+ / React Native / TypeScript — app-alexandrie story 0.1 - -### Implémentation (exemple minimal) - -```typescript -// apps/mobile/src/theme/colors.ts -export const colors = { - primary: ‘#2563EB’, - error: ‘#DC2626’, - // ... -} as const; -export type ColorToken = keyof typeof colors; - -// apps/mobile/src/theme/spacing.ts -export const spacing = { xs: 4, sm: 8, md: 12, base: 16, lg: 24 } as const; -export type SpacingToken = keyof typeof spacing; - -// apps/mobile/src/theme/index.ts (barrel export) -export * from ‘./colors’; -export * from ‘./spacing’; -export * from ‘./typography’; -export * from ‘./shadows’; -``` - -### Checklist - -- [ ] Tous les tokens `as const` pour inférence exacte -- [ ] Pas de Context React — constantes TypeScript pures -- [ ] Types dérivés (`ColorToken = keyof typeof colors`) pour l’autocomplétion -- [ ] `useFonts` dans `_layout.tsx` avec guard `!fontsLoaded` -- [ ] Fichiers TTF présents dans `assets/fonts/` et documentés dans la story - ---- - - -## Pattern : Tests de styles React Native sans renderer JSX - -### Synthèse - -- **Objectif** : tester les tokens et styles de composants React Native dans un environnement Jest `testEnvironment: node` sans renderer JSX. -- **Contexte** : config Jest avec `transform: { ‘^.+\\.ts$’: ‘ts-jest’ }` — les `.tsx` ne sont pas transformés. -- **Quand l’utiliser** : tokens de thème, logique pure, valeurs de style exportées. -- **Quand l’éviter** : rendu conditionnel (styles dynamiques inline) — nécessite `@testing-library/react-native`. - -### Analyse - -- **Avantages** : - - teste que le composant utilise les bons tokens, pas seulement que les tokens ont des valeurs - - détecte les régressions de style sans renderer - - rapide, aucune config Jest supplémentaire -- **Limites / vigilance** : - - ne teste pas le style calculé au runtime (style conditionnel dynamique) - -### Validation - -- Validé le : 19-03-2026 -- Contexte technique : React Native / Jest / ts-jest — app-alexandrie story 0.2 - -### Implémentation - -```typescript -// Button.tsx — exporter le StyleSheet avec un nom préfixé -export const buttonStyles = StyleSheet.create({ - base: { borderRadius: 20, height: 57 }, - primary: { backgroundColor: colors.primary }, -}); -export function Button(...) { ... } - -// ui-components.spec.ts — importer et vérifier les tokens -import { buttonStyles } from ‘./Button’; -import { colors } from ‘@/theme’; - -it(‘variante primary utilise colors.primary’, () => { - expect(buttonStyles.primary.backgroundColor).toBe(colors.primary); -}); -``` - -### Deux niveaux de tests UI recommandés - -1. `.spec.ts` (node) : tokens, valeurs, logique pure -2. `.spec.tsx` (config séparée avec renderer) : rendu visuel, interactions - ---- - - -## Pattern : Export des styles de composant pour réutilisation partielle (React Native) - -### Synthèse - -- **Objectif** : partager les dimensions et formes d’un composant UI vers des éléments custom qui en dérivent, sans dupliquer les valeurs. -- **Contexte** : app React Native où des screens construisent des éléments qui doivent être "au gabarit" d’un composant existant. -- **Quand l’utiliser** : bouton custom OAuth, container calqué sur un composant de base, etc. -- **Quand l’éviter** : si l’écart visuel est intentionnel — dans ce cas, une constante locale est plus claire. - -### Analyse - -- **Avantages** : - - zéro drift silencieux : si les dimensions du composant changent, tous les éléments dérivés suivent - - tests de styles possibles en dehors du composant -- **Limites / vigilance** : - - à n’utiliser que pour des éléments vraiment dérivés, pas comme contournement de design system - -### Validation - -- Validé le : 19-03-2026 -- Contexte technique : React Native / StyleSheet — app-alexandrie story 0.3 - -### Implémentation - -```typescript -// Button.tsx -export const buttonStyles = StyleSheet.create({ - base: { borderRadius: 20, height: 57 }, - primary: { backgroundColor: colors.primary }, -}); -export function Button(...) { ... } - -// login.tsx — bouton OAuth au gabarit du Button -import { buttonStyles } from ‘@/components/ui/Button’; - -``` - ---- - - -## Pattern : Token typography par usage sémantique (React Native) - -### Synthèse - -- **Objectif** : éviter les mauvais usages de tokens typography visuellement proches mais sémantiquement distincts. -- **Contexte** : fichier `typography.ts` dans un design system React Native. -- **Quand l’utiliser** : dès que deux tokens partagent la même taille mais un poids différent. -- **Quand l’éviter** : jamais — les tokens typography doivent toujours refléter l’usage, pas l’apparence. - -### Analyse - -- **Avantages** : - - prévient les "approximations" de tokens en code review - - changement de style d’usage spécifique sans régression globale -- **Limites / vigilance** : - - en review : chercher les usages sans `fontWeight` explicite — c’est souvent le signe que le mauvais token a été choisi - -### Validation - -- Validé le : 19-03-2026 -- Contexte technique : React Native / TypeScript — app-alexandrie story 0.4 - -### Implémentation - -```typescript -// Bon : nommé par usage sémantique -listItemTitle: { fontSize: 12, fontWeight: ‘600’ }, // titre d’un item de liste -caption: { fontSize: 12, fontWeight: ‘500’ }, // info secondaire, hints - -// Mauvais : nommé par apparence -mediumText12: { fontSize: 12, fontWeight: ‘500’ }, // ambigu, réutilisé à tort -``` - -**Règle** : `caption` (Medium) ≠ `listItemTitle` (SemiBold) même si la taille est identique. Ne jamais piocher un token "par approximation". - ---- - -### Principes transverses - -- Un pattern = une responsabilité claire -- On privilégie la simplicité locale avant la généricité globale -- Le code doit rester compréhensible 6 mois plus tard -- Si un pattern devient central → il mérite une décision d’architecture dédiée - -⸻ - -## Notes importantes - -- 3 bons patterns > 30 moyens -- Si un pattern évolue : - - on met à jour la date - - on précise le nouveau contexte - - En cas de doute → le pattern n’entre pas encore ici - ---- - - -## Pattern : Click-to-load strict pour les embeds tiers (iframe/widget) - -### Synthèse - -- **Objectif** : ne charger aucun service tiers sans action explicite de l’utilisateur (performance + consentement implicite). -- **Contexte** : site/webapp avec modules de réservation, map, chat ou tout embed iframe à la demande. -- **Quand l’utiliser** : dès qu’un embed tiers est chargé à la demande (pas au premier rendu). -- **Quand l’éviter** : si l’embed est central à la page et doit être visible immédiatement. - -### Analyse - -- **Avantages** : - - LCP non pollué par des tiers (performance-first) - - Aucun tiers ne reçoit de données utilisateur sans action volontaire (consentement implicite) - - Fallback toujours disponible en cas d’erreur iframe -- **Limites / vigilance** : - - Le fallback (lien externe + `tel:`) doit être actionnable même si l’embed échoue - -### Validation - -- Validé le : 21-03-2026 -- Contexte technique : React / Next.js — app-template-resto - -### Implémentation - -```tsx -const [loaded, setLoaded] = useState(false); -const [errored, setErrored] = useState(false); - -if (errored) return Ouvrir {label}; - -return ( - <> - {!loaded && } - {loaded &&