Une API sans limite de débit, c'est une porte ouverte : un script peut marteler votre endpoint de login pour deviner des mots de passe, un bot peut aspirer toutes vos données, ou un simple bug côté client peut envoyer dix mille requêtes par minute et faire tomber votre serveur. Le rate limiting est la première ligne de défense, et dans NestJS, le package officiel @nestjs/throttler le rend trivial à mettre en place.
Dans cet article, je détaille comment limiter le débit de votre API NestJS proprement : configuration globale, limites différenciées par route, exclusions, stockage distribué avec Redis pour les déploiements multi-instances, et les spécificités GraphQL. L'objectif : que votre backend reste debout même sous la pression, sans pénaliser vos utilisateurs légitimes.
Pourquoi le rate limiting n'est pas optionnel
Beaucoup de développeurs repoussent le rate limiting à « plus tard, quand on aura du trafic ». C'est une erreur, car les abus ne dépendent pas de votre succès : un endpoint public est scanné par des bots dans les heures qui suivent sa mise en ligne. Limiter le débit répond à trois besoins concrets.
Le premier est la sécurité. Les attaques par force brute sur l'authentification, le bourrage d'identifiants (credential stuffing) et l'énumération de comptes reposent toutes sur la capacité d'envoyer un grand nombre de requêtes. En plafonnant le nombre de tentatives par fenêtre de temps, vous rendez ces attaques impraticables.
Le deuxième est la stabilité. Sans garde-fou, un seul client mal codé peut saturer votre base de données ou vos workers. Le rate limiting agit comme un fusible : il coupe avant que la surcharge ne se propage à toute l'application.
Le troisième est le coût. Si vous facturez des appels à des API tierces (un service d'envoi d'emails, une API de paiement, un modèle d'IA), chaque requête abusive vous coûte de l'argent. Plafonner le débit protège directement votre marge.
Installation et configuration globale
Commencez par installer le package officiel :
npm install @nestjs/throttler
Ensuite, importez ThrottlerModule dans votre module racine. Depuis la v5, la configuration repose sur un tableau de « throttlers » nommés, ce qui permet de définir plusieurs fenêtres de temps en parallèle :
import { Module } from '@nestjs/common';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // fenêtre de 1 seconde
limit: 3, // 3 requêtes max
},
{
name: 'long',
ttl: 60000, // fenêtre de 60 secondes
limit: 100, // 100 requêtes max
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
En enregistrant ThrottlerGuard comme APP_GUARD, vous l'appliquez à toutes les routes de l'application d'un coup. Les deux throttlers ci-dessus se cumulent : un client ne peut dépasser ni 3 requêtes par seconde, ni 100 requêtes par minute. Cette double fenêtre est très utile : la fenêtre courte bloque les rafales, la fenêtre longue limite le volume global.
Quand un client dépasse la limite, NestJS renvoie automatiquement un statut HTTP 429 Too Many Requests, accompagné des en-têtes standard Retry-After et X-RateLimit-* qui indiquent quand réessayer.
Ajuster les limites route par route
La configuration globale est une base, mais toutes les routes n'ont pas les mêmes besoins. Un endpoint de login doit être bien plus strict qu'une route de lecture publique. Les décorateurs @Throttle() et @SkipThrottle() permettent de surcharger le comportement au niveau d'un contrôleur ou d'une méthode.
import { Controller, Post, Get, Body } from '@nestjs/common';
import { Throttle, SkipThrottle } from '@nestjs/throttler';
@Controller('auth')
export class AuthController {
// Limite stricte : 5 tentatives par minute sur le login
@Throttle({ default: { limit: 5, ttl: 60000 } })
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
// Cette route ignore complètement le rate limiting
@SkipThrottle()
@Get('health')
healthCheck() {
return { status: 'ok' };
}
}
Vous pouvez aussi cibler un throttler nommé précis. Si vous voulez assouplir uniquement la fenêtre courte sur une route de recherche en autocomplétion (qui génère beaucoup de requêtes légitimes), vous surchargez seulement short :
@Throttle({ short: { limit: 20, ttl: 1000 } })
@Get('search')
search(@Query('q') query: string) {
return this.searchService.find(query);
}
Cette granularité est précieuse : vous resserrez là où le risque est élevé (authentification, création de ressources) et vous relâchez là où le trafic légitime est naturellement intense.
Identifier le bon client : au-delà de l'adresse IP
Par défaut, le throttler identifie chaque client par son adresse IP. C'est suffisant pour une API simple, mais cela pose deux problèmes en production. D'abord, derrière un proxy ou un load balancer, toutes les requêtes arrivent avec l'IP du proxy, ce qui ferait partager la même limite à tous vos utilisateurs. Ensuite, pour une API authentifiée, il est souvent plus juste de limiter par utilisateur que par IP.
Pour résoudre le premier point, assurez-vous que votre application fait confiance au proxy. Avec Express, activez trust proxy afin que req.ip reflète l'IP réelle transmise dans l'en-tête X-Forwarded-For :
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy', 1); // fait confiance au premier proxy
await app.listen(3000);
}
bootstrap();
Pour le second point, créez un guard personnalisé qui dérive de ThrottlerGuard et surcharge la méthode de génération du « tracker ». Vous renvoyez l'identifiant de l'utilisateur authentifié quand il existe, et vous retombez sur l'IP sinon :
import { Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
@Injectable()
export class UserThrottlerGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
// Si l'utilisateur est authentifié, on limite par son id
return req.user?.id ? `user-${req.user.id}` : req.ip;
}
}
N'oubliez pas de remplacer ThrottlerGuard par UserThrottlerGuard dans la déclaration APP_GUARD. Cette approche évite qu'un utilisateur légitime partageant une IP d'entreprise soit pénalisé par le trafic de ses collègues.
Stockage distribué avec Redis
Le stockage par défaut du throttler est en mémoire. Cela fonctionne parfaitement pour une instance unique, mais devient un piège dès que vous déployez plusieurs instances derrière un load balancer. Chaque instance compte les requêtes de son côté : avec trois instances, un client peut effectivement tripler sa limite réelle, puisque ses requêtes sont réparties entre les processus.
La solution est un stockage partagé. Le package @nest-lab/throttler-storage-redis centralise le comptage dans Redis, qui devient la source de vérité commune à toutes les instances :
npm install @nest-lab/throttler-storage-redis ioredis
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import Redis from 'ioredis';
@Module({
imports: [
ThrottlerModule.forRoot({
throttlers: [{ name: 'long', ttl: 60000, limit: 100 }],
storage: new ThrottlerStorageRedisService(
new Redis({ host: 'localhost', port: 6379 }),
),
}),
],
})
export class AppModule {}
Avec cette configuration, peu importe le nombre d'instances : le compteur est unique et cohérent. C'est indispensable dès que votre application tourne sur Kubernetes, sur plusieurs conteneurs, ou sur un service auto-scalé. Si vous containerisez déjà votre stack, c'est le moment d'y ajouter un service Redis dédié.
Le cas particulier de GraphQL
Si vous exposez une API GraphQL avec NestJS, le throttler nécessite un petit ajustement. Le contexte d'exécution GraphQL n'expose pas l'objet requête de la même manière qu'en REST, il faut donc l'extraire explicitement dans un guard dédié :
import { ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { ThrottlerGuard } from '@nestjs/throttler';
@Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
getRequestResponse(context: ExecutionContext) {
const gqlCtx = GqlExecutionContext.create(context);
const ctx = gqlCtx.getContext();
return { req: ctx.req, res: ctx.res };
}
}
Gardez en tête une limite conceptuelle : en GraphQL, tout passe par un seul endpoint POST. Le rate limiting par route perd donc de sa pertinence, puisqu'une requête « légère » et une requête « lourde » comptent pareil. Pour une protection plus fine, le rate limiting GraphQL se combine idéalement avec une analyse de complexité des requêtes, qui plafonne le coût calculé d'une requête plutôt que leur simple nombre.
Tester et observer son rate limiting
Mettre en place une protection sans la vérifier, c'est se donner une fausse sécurité. Un test d'intégration simple confirme que la limite se déclenche bien. Avec un client HTTP, envoyez plus de requêtes que la limite et vérifiez le passage au statut 429 :
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
it('renvoie 429 après dépassement de la limite', async () => {
const limit = 5;
// On envoie une requête de plus que la limite autorisée
for (let i = 0; i < limit; i++) {
await request(app.getHttpServer()).post('/auth/login').expect(401);
}
await request(app.getHttpServer())
.post('/auth/login')
.expect(429); // la requête de trop est bloquée
});
En production, surveillez le taux de réponses 429 dans vos métriques. Un pic soudain peut signaler une attaque en cours, mais aussi une limite trop agressive qui gêne vos vrais utilisateurs. Le bon réglage se trouve toujours par itération : commencez large, observez le trafic réel, puis resserrez. Coupler ces métriques à un outil de monitoring vous donne l'alerte au bon moment.
Conclusion
Le rate limiting n'est pas une optimisation que l'on ajoute en fin de projet : c'est une protection de base qui doit accompagner toute API exposée. Avec @nestjs/throttler, vous couvrez l'essentiel en quelques lignes : une configuration globale à double fenêtre pour bloquer les rafales et plafonner le volume, des limites strictes sur les routes sensibles comme l'authentification, une identification du client adaptée à votre contexte, et un stockage Redis dès que vous passez à plusieurs instances.
Retenez la progression : sécurisez d'abord globalement, affinez ensuite route par route, puis distribuez le comptage quand votre infrastructure grandit. Cette montée en charge progressive vous évite à la fois la sur-ingénierie au démarrage et les mauvaises surprises en production.
Pour aller plus loin, ce sujet s'articule bien avec la sécurisation de vos endpoints via les Guards NestJS avec JWT et rôles, la validation des entrées avec les Pipes et Interceptors NestJS, et une API GraphQL Code First type-safe.
Vous avez un projet web ou une API à sécuriser ? N'hésitez pas à me contacter pour en discuter, ou écrivez-moi directement à contact@alexis-mouchon.fr.