Files
_Assistant_Lead_Tech/knowledge/backend/patterns/nestjs.md
MaksTinyWorkshop fc0bec0e2b capitalisation: intégrer 12 entrées depuis app-alexandrie et app-template-resto
- backend/risques/nestjs : guard multi-statut READ_METHODS avant statut
- backend/patterns/nestjs : fusionner lastSeenAt dans la réconciliation
- backend/risques/contracts : pas de process.env dans services/helpers
- backend/risques/nextjs : self-request Server Action + EXDEV atomic write
- backend/risques/prisma : champ enum-like stocké en String
- frontend/risques/general : Alert.prompt iOS-only
- frontend/risques/tests : 3 anti-patterns (helpers copiés, test indirect, test façade)
- workflow/risques/story-tracking : 2 entrées (hors périmètre, File List approximative)
- skill capitalisation-triage : nouveau format de rapport (tableaux par domaine)
- 95_a_capitaliser.md : purgé
2026-03-31 14:47:42 +02:00

6.7 KiB

Backend — Patterns : NestJS

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/patterns/README.md pour l'index complet.


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)

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

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

Pattern : Fusionner lastSeenAt dans l'update de réconciliation — évite N requêtes DB par requête

  • Objectif : éviter deux appels Prisma distincts (réconciliation + lastSeenAt) sur chaque requête authentifiée.
  • Contexte : service de réconciliation d'état de session appelé à chaque request via guard ou middleware.
  • Quand l'utiliser : dès qu'un lastSeenAt est mis à jour systématiquement et qu'un update conditionnel coexiste.
  • Avantage : 1 requête DB par requête authentifiée au lieu de 2.
  • Validé le : 30-03-2026
  • Contexte technique : NestJS / Prisma — app-alexandrie

Implémentation (exemple minimal)

// ❌ 2 requêtes par requête authentifiée
private async reconcileSessionStatus(session) {
  if (statusChanged) await prisma.session.update({ data: { status, graceEndsAt } });
}
await prisma.session.update({ data: { lastSeenAt: now } }); // 2ème update systématique

// ✅ 1 requête — lastSeenAt toujours inclus dans le même appel
private async reconcileSessionStatus(session, now = new Date()) {
  await prisma.session.update({
    data: { lastSeenAt: now, ...(statusChanged && { status, graceEndsAt }) }
  });
}