Rate limiting avec Throttler dans NestJS

Rate limiting avec Throttler dans NestJS

Une API publique exposée sur Internet est constamment sollicitée : visiteurs légitimes, bots, scripts d'attaque par force brute, scrapers… Sans protection, un simple endpoint de connexion ou de contact peut être saturé en quelques secondes, voire exploité pour deviner des mots de passe ou spammer une boîte mail. Le rate limiting (limitation du débit) est la première ligne de défense contre ces abus.

Dans cet article, nous allons voir comment mettre en place une protection robuste avec @nestjs/throttler, le module officiel de NestJS dédié au rate limiting. Nous nous appuierons sur la configuration réelle d'un projet en production (le site site-voyance) pour illustrer les bonnes pratiques.

Qu'est-ce que le rate limiting et pourquoi est-ce essentiel ?

Le rate limiting consiste à limiter le nombre de requêtes qu'un client peut effectuer pendant une fenêtre de temps donnée. Par exemple : « pas plus de 100 requêtes par minute par adresse IP ». Si la limite est dépassée, le serveur renvoie une réponse 429 Too Many Requests.

Cette protection est essentielle pour plusieurs raisons :

  • Sécurité : empêcher les attaques par force brute sur les endpoints d'authentification
  • Anti-spam : éviter qu'un formulaire de contact soit utilisé pour envoyer des centaines de messages
  • Stabilité : protéger l'infrastructure contre une surcharge accidentelle ou intentionnelle (DoS)
  • Coûts : limiter la consommation de ressources (CPU, base de données, services tiers facturés à la requête)

Prérequis

  • Un projet NestJS fonctionnel (v10 ou supérieur)
  • Node.js 18+
  • Des notions de base sur les modules et les guards de NestJS

Installation du module Throttler

L'installation se fait via npm :

npm install --save @nestjs/throttler

Aucune dépendance externe n'est requise : par défaut, Throttler stocke les compteurs en mémoire. Pour une application distribuée sur plusieurs instances, il sera nécessaire d'utiliser un stockage partagé comme Redis (nous y reviendrons).

Configuration du ThrottlerModule

La configuration se fait dans le module racine de l'application. Voici la mise en place utilisée dans le projet site-voyance :

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
    // Rate limiting
    ThrottlerModule.forRoot([
      {
        ttl: 60000, // 1 minute
        limit: 100, // 100 requests per minute
      },
    ]),
    // ... autres modules
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

Deux paramètres définissent la règle :

  • ttl (Time To Live) : la fenêtre de temps en millisecondes. Ici 60 000 ms, soit 1 minute.
  • limit : le nombre maximum de requêtes autorisées pendant cette fenêtre.

⚠️ Attention : depuis la version 5 de @nestjs/throttler, le ttl s'exprime en millisecondes (et non plus en secondes comme dans les versions antérieures). Une erreur fréquente consiste à mettre 60 au lieu de 60000, ce qui aboutit à une limite quasi instantanée.

Application globale avec APP_GUARD

L'astuce essentielle dans la configuration ci-dessus est l'enregistrement du ThrottlerGuard via le token APP_GUARD :

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard,
}

Ce pattern indique à NestJS d'appliquer ce guard globalement, sur toutes les routes de l'application, sans avoir à le déclarer dans chaque contrôleur. C'est une approche « secure by default » : toute nouvelle route est automatiquement protégée.

Configurations multiples : différentes limites selon les routes

Toutes les routes n'ont pas les mêmes besoins. Une page d'accueil peut tolérer de nombreuses requêtes, alors qu'un endpoint de connexion doit être strictement limité. Throttler permet de définir plusieurs profils nommés :

ThrottlerModule.forRoot([
  {
    name: 'short',
    ttl: 1000,    // 1 seconde
    limit: 3,     // 3 requêtes par seconde
  },
  {
    name: 'medium',
    ttl: 10000,   // 10 secondes
    limit: 20,
  },
  {
    name: 'long',
    ttl: 60000,   // 1 minute
    limit: 100,
  },
]),

Avec cette configuration, un client doit respecter simultanément les trois règles : pas plus de 3 req/s, pas plus de 20 req/10s, et pas plus de 100 req/min. Cela permet d'absorber des pics courts tout en empêchant les attaques soutenues.

Limites personnalisées avec @Throttle()

Pour surcharger la limite globale sur une route précise, on utilise le décorateur @Throttle(). C'est particulièrement utile sur les endpoints sensibles. Dans le projet, le contrôleur d'authentification applique des limites strictes :

import { Throttle } from '@nestjs/throttler';
import { Public } from '../../common/decorators/public.decorator';

@Controller('auth')
export class AuthController {
  @Post('login')
  @Public()
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  async login(@Body() loginDto: LoginDto): Promise<ApiResponseDto<any>> {
    const result = await this.authService.login(loginDto);
    return ApiResponseDto.success(result, 'Login successful');
  }

  @Post('register')
  @Public()
  @Throttle({ default: { limit: 5, ttl: 3600000 } })
  async register(@Body() registerDto: RegisterDto): Promise<ApiResponseDto<any>> {
    const user = await this.authService.register(registerDto);
    return ApiResponseDto.success(user, 'User registered successfully');
  }
}

On voit ici deux stratégies :

  • Login : 5 tentatives par minute. Cela bloque efficacement les attaques par force brute sur les mots de passe.
  • Register : 5 inscriptions par heure (3 600 000 ms). Évite la création massive de comptes par des bots.

Le formulaire de contact suit la même logique :

@Post()
@Public()
@Throttle({ default: { limit: 3, ttl: 60000 } })
@ApiOperation({ summary: 'Send a contact message' })
async create(
  @Body() createContactDto: CreateContactDto,
): Promise<ApiResponseDto<ContactMessage>> {
  const message = await this.contactService.create(createContactDto);
  return ApiResponseDto.success(
    message,
    'Merci pour votre message ! Je vous réponds très vite.',
  );
}

Trois messages par minute suffisent largement pour un usage légitime, mais bloquent les spammeurs automatisés.

La clé default dans l'objet correspond au nom du throttler. Si vous avez défini plusieurs profils nommés (short, medium...), vous pouvez surcharger précisément celui qui vous intéresse.

Exclure certaines routes avec @SkipThrottle()

Certaines routes doivent être exemptées du rate limiting : webhooks de paiement, health checks, ou endpoints internes. Le décorateur @SkipThrottle() remplit ce rôle :

import { SkipThrottle } from '@nestjs/throttler';

@Controller('health')
export class HealthController {
  @Get()
  @SkipThrottle()
  check() {
    return { status: 'ok' };
  }
}

On peut aussi ne désactiver qu'un profil spécifique : @SkipThrottle({ short: true }) permet d'ignorer la limite courte tout en conservant les limites moyennes et longues.

Personnaliser la clé de tracking

Par défaut, Throttler identifie chaque client par son adresse IP. Mais derrière un proxy (Nginx, Cloudflare, load balancer), toutes les requêtes peuvent sembler venir de la même IP. Pour adapter ce comportement, on peut étendre le ThrottlerGuard :

import { Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  protected async getTracker(req: Record<string, any>): Promise<string> {
    // Si l'utilisateur est authentifié, on track par son ID
    if (req.user?.id) {
      return `user-${req.user.id}`;
    }
    // Sinon, on utilise l'IP réelle (X-Forwarded-For derrière un proxy)
    return req.ips.length ? req.ips[0] : req.ip;
  }
}

N'oubliez pas d'activer la confiance des proxies dans le bootstrap de l'application :

const app = await NestFactory.create(AppModule);
app.set('trust proxy', 1); // si Express
await app.listen(3000);

Puis remplacez le guard global par votre version personnalisée :

{
  provide: APP_GUARD,
  useClass: CustomThrottlerGuard,
}

Headers de réponse

Le module Throttler ajoute automatiquement des en-têtes HTTP standardisés à chaque réponse :

  • X-RateLimit-Limit : nombre maximum de requêtes autorisées sur la fenêtre
  • X-RateLimit-Remaining : nombre de requêtes restantes
  • X-RateLimit-Reset : timestamp Unix de la prochaine réinitialisation du compteur
  • Retry-After : présent uniquement en cas de réponse 429, indique le délai (en secondes) à attendre

Ces en-têtes permettent aux clients d'adapter leur comportement, par exemple en affichant un message d'attente plutôt qu'une erreur brutale. Côté frontend, on peut intercepter les réponses 429 et planifier un retry automatique en se basant sur Retry-After.

Aller plus loin : stockage Redis

Si votre application est déployée sur plusieurs instances (cluster, Kubernetes), le stockage par défaut en mémoire pose problème : chaque instance a son propre compteur, ce qui multiplie de fait la limite réelle. La solution est d'utiliser un stockage partagé avec @nest-lab/throttler-storage-redis :

npm install @nest-lab/throttler-storage-redis ioredis
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import Redis from 'ioredis';

ThrottlerModule.forRoot({
  throttlers: [{ ttl: 60000, limit: 100 }],
  storage: new ThrottlerStorageRedisService(new Redis()),
}),

Conclusion

Avec quelques lignes de configuration, @nestjs/throttler apporte une protection efficace contre les abus les plus courants. Les points clés à retenir :

  • Activez Throttler globalement via APP_GUARD pour une protection par défaut
  • Définissez une limite globale raisonnable (100 req/min est un bon point de départ)
  • Surchargez avec @Throttle() sur les endpoints sensibles : login, register, contact
  • Utilisez @SkipThrottle() pour les health checks et webhooks
  • En production multi-instances, passez sur un stockage Redis partagé
  • Pour les utilisateurs authentifiés, trackez par ID utilisateur plutôt que par IP

Pour aller plus loin, vous pouvez consulter la documentation officielle de NestJS sur le rate limiting, et envisager de coupler Throttler avec d'autres mesures de sécurité comme Helmet (en-têtes de sécurité), CSRF protection, ou un WAF en amont. La sécurité fonctionne par couches, et le rate limiting en est une fondation indispensable.

Commentaires

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Les champs obligatoires sont indiqués avec *