mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 13:31:43 +02:00
34 KiB
34 KiB
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 : 16-03-2026
Index
- Format d’erreur API standardisé
- Middleware de corrélation (requestId / traceId)
- Idempotency key pour opérations sensibles
- Pagination robuste (cursor-based) pour les listings
- Exécution asynchrone des tâches longues (queue + outbox light)
- Soft delete et archivage explicite
- Webhooks sortants robustes et idempotents
- Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)
- Guard global NestJS — ordre d’enregistrement et décorateurs de bypass
- Provider-Strategy pour intégrations tierces — périmètre complet
- Stripe — metadata sur
subscription_data, pas sur la Session - Webhooks entrants — parsing unique (single constructWebhookEvent)
- Contracts-First — error codes comme contrat obligatoire
- RedisHealthService avec cache interne court
- Sémantique explicite
TrialvsPaiddans Subscription - Restauration d’achats Stripe en 3 étapes
- Mapping explicite de
P2002Prisma sur update de champ unique - Autorisation interne minimale sans RBAC complet
- Anti-énumération sur endpoints auth liés à un email
- Token à usage unique — génération, hash et invalidation atomique
- Guardrails multi-tenant — 403 vs 404 selon la sémantique
- Repository tenant-aware —
tenantIdobligatoire dans la signature - Défense en profondeur — inclure
tenantIddans les 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 16ouPython 3.12 / FastAPI / Redis
Implémentation (exemple minimal)
(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)
{
"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)
- 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)
- 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)
- 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)
- 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)
- 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)
- É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/contractsou é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
- Ne pas mettre de logique métier dans
- 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 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<typeof Schema> ZodValidationPipeglobal 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_GUARDdansproviders[]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
- L’ordre des
- 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 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)
// 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 eventscustomer.subscription.*, pas seulement danscheckout.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.userIdabsent des eventscustomer.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
constructWebhookEventune 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
// 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/contractspour é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.tsavant 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 danspackages/contracts/src/errors/error-code.tsen 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
- réduit fortement le flood de
- 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 l’abonnement payant actif.
- Contexte : modèle
SubscriptionoùtrialEndsAtmaté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
isActivedoit préciser si elle signifie “trial ou paid” ou “paid only”
- toute logique
- Validé le : 10-03-2026
- Contexte technique : Backend agnostique / modèle d’abonnement
Implémentation (exemple minimal)
- 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)
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
stripeCustomerIdpersistant 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 :
updatePrisma sur un champ@uniquealimenté 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)
- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format d’erreur standardisé
Checklist
P2002intercepté 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)
- 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)
- 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)
- 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 : 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)
- `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)
- 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
tenantIdobligatoire 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 :
updateouupdateManysur une ressource tenant-scoped. - Quand l’utiliser : dès qu’une mutation dépend d’un
idreç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
tenantIdsoit 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
tenantIdprésent dans les clauseswheredes updates sensibles- Pas de mutation tenant-scoped basée sur
idseul - Revue explicite des exceptions documentées