# 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) ```typescript // app.module.ts providers: [ { provide: APP_GUARD, useClass: AuthGuard }, // 1er : peuple request.user { provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user { provide: APP_GUARD, useClass: EntitlementsGuard }, // 3ème : lit request.user + entitlements ] // skip-auth.decorator.ts export const SKIP_AUTH = 'skipAuth'; export const SkipAuth = () => SetMetadata(SKIP_AUTH, true); // auth.guard.ts const skip = this.reflector.getAllAndOverride(SKIP_AUTH, [ context.getHandler(), context.getClass(), // permet @SkipAuth() au niveau classe ]); if (skip) return true; ``` ### Checklist - [ ] AuthGuard enregistré en premier dans `providers[]` - [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule - [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs) - [ ] Tests unitaires sur le guard avec reflector mocké --- ## Pattern : RedisHealthService avec cache interne court - Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête. - Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision d'écriture. - Quand l'utiliser : quand plusieurs requêtes concurrentes doivent consulter l'état Redis. - Quand l'éviter : si Redis n'est pas consulté dans le chemin request/response. - Avantage : - réduit fortement le flood de `PING` - garde un signal d'état suffisamment frais - Limites / vigilance : - la fenêtre de cache doit rester courte - l'état initial doit être explicite et assumé - Validé le : 10-03-2026 - Contexte technique : NestJS / Redis ### Implémentation (exemple minimal) ```txt - Mémoriser lastStatus et lastCheck - Si le dernier check a moins de 5s, retourner l'état en cache - Sinon exécuter un vrai PING et mettre le cache à jour - Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot ``` ### Checklist - Cache court documenté - Pas de ping Redis à chaque requête - Comportement initial explicite --- ## Pattern : 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) ```typescript // RedisService — méthode dédiée async incrWithExpireAt(key: string, expireAtMs: number): Promise { 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)