Files
_Assistant_Lead_Tech/10_backend_patterns_valides.md
2026-03-20 13:59:42 +01:00

45 KiB
Raw Blame History

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 : 20-03-2026


Index


Règle dor

Si ce nest pas confirmé comme fonctionnel et utile, ça na rien à faire ici.

  • Pas de “bonnes pratiques” vagues
  • Pas de dépendances implicites à une stack
  • Si cest 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 derreurs
  • DB & migrations
  • observabilité
  • opérations sensibles (idempotence, retries)
  • intégrations (webhooks, jobs async)

Format standard dun pattern

Pattern :

  • Objectif : …
  • Contexte : …
  • Quand lutiliser : …
  • 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)

(contenu)

Checklist (si pertinente)

  • Erreurs standardisées
  • Validation dentrée (schéma)
  • Observabilité minimale (requestId/traceId + logs)
  • Sécurité (authn/authz + secrets)
  • Tests au bon niveau
  • Idempotence si opération sensible

Pattern : Format derreur 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 lutiliser : dès quune API est exposée à autre chose quun 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)

{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "Utilisateur introuvable",
    "requestId": "abc-123"
  }
}

Checklist

  • Codes HTTP cohérents (4xx / 5xx)
  • Codes derreur 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 lutiliser : 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)

- Générer un requestId à lentrée si absent
- Le propager dans le contexte de requête
- Linclure dans chaque log et réponse derreur

Checklist

  • requestId généré ou repris dun 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 lutiliser : 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)

- Client fournit Idempotency-Key
- Backend stocke la clé + résultat
- Retry retourne le résultat initial

Checklist

  • Clé obligatoire sur endpoints sensibles
  • Contrainte dunicité 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 lutiliser : dès quun listing peut dépasser quelques dizaines/centaines ditems ou subir des écritures concurrentes.
  • Quand léviter : listes strictement petites et statiques.
  • Avantage :
    • Résultats stables malgré insertions/suppressions
    • Meilleure performance que loffset 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)

- 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 demails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
  • Quand lutiliser : dès quune opération peut dépasser la latence acceptable ou dépendre dun 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 lidempotence
    • Nécessite une stratégie minimale de dead-letter ou dalerting
  • Validé le : 25-01-2026
  • Contexte technique : Backend agnostique + DB transactionnelle + worker

Implémentation (exemple minimal)

- 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 daudit, restauration ou conformité.
  • Quand lutiliser : dès quune 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)

- 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 lutiliser : dès quun é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)

- É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 dinterface 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 lutiliser : dès quune 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)

// 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<typeof RegisterRequestSchema>; // 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 derreur 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 derreur applicatifs stables
  • Enums et constantes partagées (ex : liste officielle de sujets/topics)

Ce qui nappartient PAS à contracts

  • Logique métier
  • Modules/services/guards framework (NestJS, etc.)
  • State management client (Zustand, Redux, etc.)

Checklist

  • Zéro DTO manuel dans lAPI — uniquement z.infer<typeof Schema>
  • ZodValidationPipe global ou par endpoint pour la validation dentrée
  • Constantes partagées (enums, listes) dans contracts, jamais dupliquées
  • Mocks de tests importent les types depuis contracts

Pattern : Guard global NestJS — ordre denregistrement 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 lutiliser : dès quon a 2+ guards globaux dont lun dépend du résultat de lautre.
  • Quand léviter : si un seul guard suffit.
  • Avantage :
    • Sécurité par défaut (opt-out, pas opt-in)
    • Ordre dexécution garanti et explicite
    • Bypass documenté et traçable via décorateurs
  • Limites / vigilance :
    • Lordre des APP_GUARD dans providers[] est lordre dexécution — ne pas inverser
    • Exporter le service depuis son module si injecté dans un guard global dun autre module
  • Validé le : 07-03-2026
  • Contexte technique : NestJS v10+

Implémentation (exemple minimal)

// 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<boolean>(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 lutiliser : dès quun service applicatif dépend dun SDK tiers (et plus encore sil 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 :
    • Linterface 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)

// 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<void>;
  handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
}

// billing.service.ts (domaine uniquement)
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
  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 lutiliser : systématiquement dès quon 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

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 lutiliser : dès quon 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

// 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 derreur 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 dimporter lenum → 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 lenum, pas une string littérale
  • PR review : vérifier error-code.ts à chaque ajout dendpoint derreur

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 lutiliser : quand plusieurs requêtes concurrentes doivent consulter létat Redis.
  • Quand léviter : si Redis nest 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)

- 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 labonnement payant actif.
  • Contexte : modèle SubscriptiontrialEndsAt matérialise un essai.
  • Quand lutiliser : dès quun 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 dabonnement

Implémentation (exemple minimal)

- Un abonnement payant actif nest pas seulement status = ACTIVE
- Il doit aussi avoir trialEndsAt = null
- Les fixtures et mocks e2e dun 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 dachats Stripe en 3 étapes

  • Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre dune hypothèse fragile.
  • Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé.
  • Quand lutiliser : dès quun utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation.
  • Quand léviter : si létat Stripe nest pas la source de vérité.
  • Avantage :
    • rend la réconciliation explicite
    • supporte retries et restaurations tardives
  • Limites / vigilance :
    • la pagination Stripe et lidempotence décriture restent obligatoires
  • Validé le : 10-03-2026
  • Contexte technique : Stripe API / backend Node/NestJS

Implémentation (exemple minimal)

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 dunicité prévisible en erreur métier exploitable plutôt quen 500 opaque.
  • Contexte : update Prisma sur un champ @unique alimenté par une source externe ou concurrente.
  • Quand lutiliser : dès quun 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 derreur API standardisé
  • Validé le : 10-03-2026
  • Contexte technique : Prisma / PostgreSQL / NestJS

Implémentation (exemple minimal)

- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format derreur standardisé

Checklist

  • P2002 intercepté sur les updates sensibles
  • Code derreur 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 dune capacité admin ou opérateur clairement identifiée.
  • Quand lutiliser : 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 dune 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)

- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN)
- propager ce rôle dans la session ou le token dauth
- 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 dauth existant
  • Les endpoints sensibles passent par un guard dédié
  • Aucun contrôle daccès critique nest 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 dutilisation.

Pattern : Anti-énumération sur endpoints auth liés à un email

  • Objectif : empêcher quun endpoint auth révèle si un compte existe, nexiste pas ou nest pas éligible.
  • Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part dun email utilisateur.
  • Quand lutiliser : dès quune requête auth touche un identifiant de type email.
  • Quand léviter : jamais sur une surface exposée.
  • Avantage :
    • réduit la fuite dinformation 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 lexposer au client
  • Validé le : 16-03-2026
  • Contexte technique : Node.js / auth applicative / API HTTP

Implémentation (exemple minimal)

- retourner la même réponse HTTP 200 quun 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 dexistence dans le message ou le code derreur
  • 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 demail, lien magique ou tout token one-shot.
  • Quand lutiliser : pour tout token à usage unique transmis à lutilisateur.
  • Quand léviter : sessions longues ou secrets devant être relus en clair côté serveur.
  • Avantage :
    • réduit limpact dune fuite de base
    • garde des tokens URL-safe
    • favorise une consommation atomique et réutilisable
  • Limites / vigilance :
    • la consommation doit rester atomique
    • la politique dexpiration doit être explicite
  • Validé le : 16-03-2026
  • Contexte technique : Node.js crypto / Prisma / email ou URL signée

Implémentation (exemple minimal)

- 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 dorchestration.
  • Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node.
  • Quand lutiliser : dès quun 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 dinjection doivent rester simples pour éviter un excès dabstraction
  • Validé le : 19-03-2026
  • Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables

Implémentation (exemple minimal)

- 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 dorchestration 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 lorchestrateur plus légèrement

Checklist

  • server-only absent des modules de logique pure
  • APIs Next.js runtime-only limitées aux couches dentré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 dinformation inter-tenant tout en gardant une sémantique derreur claire.
  • Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
  • Quand lutiliser : dès quune vérification dappartenance tenant peut soit refuser explicitement laccès, soit masquer lexistence dune 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 disolation 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)

- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais laccès refusé
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer lexistence dune ressource dun 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 dexistence 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 daccès aux données sur ressources tenant-scoped.
  • Quand lutiliser : dès quun 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 dun second garde-fou dans les mutations sensibles
  • Validé le : 16-03-2026
  • Contexte technique : TypeScript / Prisma / architecture repository

Implémentation (exemple minimal)

- 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 lutiliser : dès quune mutation dépend dun 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 limpact dun 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 dautorisation
    • 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)

- 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

- `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)

// 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)

// 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)

// RedisService — méthode dédiée
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
  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)