# 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 : 19-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 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) --- ## 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 update de champ unique - Objectif : transformer un conflit d’unicité prévisible en erreur métier exploitable plutôt qu’en 500 opaque. - Contexte : `update` Prisma sur un champ `@unique` alimenté par une source externe ou concurrente. - Quand l’utiliser : dès qu’un champ unique peut être mis à jour après création. - 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é ``` ### Checklist - `P2002` intercepté sur les updates sensibles - Code d’erreur métier stable - 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