mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
968 lines
37 KiB
Markdown
968 lines
37 KiB
Markdown
# 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 : <Nom clair>
|
||
|
||
- 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
|
||
|
||
---
|
||
|
||
<a id="pattern-format-derreur-api-standardise"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-middleware-correlation-requestid-traceid"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-idempotency-key-operations-sensibles"></a>
|
||
|
||
## 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é
|
||
|
||
---
|
||
|
||
<a id="pattern-pagination-robuste-cursor-based"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-execution-asynchrone-taches-longues"></a>
|
||
|
||
## 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)
|
||
|
||
---
|
||
|
||
<a id="pattern-soft-delete-archivage-explicite"></a>
|
||
|
||
## 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é
|
||
|
||
---
|
||
|
||
<a id="pattern-webhooks-sortants-robustes-idempotents"></a>
|
||
|
||
## 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)
|
||
|
||
---
|
||
|
||
<a id="pattern-contracts-first-zod-infer-no-dto"></a>
|
||
|
||
## 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<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>`
|
||
- [ ] `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
|
||
|
||
---
|
||
|
||
<a id="pattern-guard-global-nestjs"></a>
|
||
|
||
## 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<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é
|
||
|
||
---
|
||
|
||
<a id="pattern-provider-strategy-integrations-tierces"></a>
|
||
|
||
## 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<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 */ });
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
<a id=”pattern-stripe-subscription-metadata”></a>
|
||
|
||
## 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.*
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
<a id=”pattern-webhook-parsing-unique”></a>
|
||
|
||
## 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
|
||
```
|
||
|
||
---
|
||
|
||
<a id=”pattern-contracts-error-codes”></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-redis-health-cache-court"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-subscription-trial-vs-paid"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-restauration-achats-stripe"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-prisma-p2002-update-unique"></a>
|
||
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-autorisation-interne-minimale"></a>
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
<a id="pattern-anti-enumeration-auth-email"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-token-usage-unique"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-nextjs-runtime-only-logique-pure-testable"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-guardrails-multi-tenant-403-404"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-repository-tenant-aware"></a>
|
||
## 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
|
||
|
||
---
|
||
|
||
<a id="pattern-tenantid-dans-updates"></a>
|
||
## 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
|