mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
138
knowledge/backend/patterns/nestjs.md
Normal file
138
knowledge/backend/patterns/nestjs.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Backend — Patterns : NestJS
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<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-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-quota-redis-atomique"></a>
|
||||
## 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<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)
|
||||
Reference in New Issue
Block a user