When building a REST API, we must prepare for various cases. At first, everything might run smoothly, but as our API gains traction, we might encounter some issues. We might run into users who begin sending hundreds or thousands of requests per second, pulling data far more often than necessary. This might cause our server to struggle to keep up. This might make our API slow or unresponsive for other users. We can also encounter attackers launching a Denial of Service (DoS) attack, flooding our API with millions of requests, and crashing our system intentionally.
To solve the above issues, we can implement rate limiting. It ensures that a single user can’t overwhelm our API by allowing us to limit how frequently they can make API requests. With this approach, our API can remain fast and reliable, keeping attackers at bay while allowing legitimate users to access the data they need.
Configuring the Throttler module
One of the ways to implement rate limiting with NestJS is to use the provided Throttler module.
1npm install @nestjs/throttlerWith it, we can specify how many HTTP requests a particular user can make within a given time.
1import { Module } from '@nestjs/common';
2import { ThrottlerModule } from '@nestjs/throttler';
3
4@Module({
5 imports: [
6 ThrottlerModule.forRoot({
7 throttlers: [
8 {
9 ttl: 60000,
10 limit: 10,
11 },
12 ],
13 }),
14 // ...
15 ],
16})
17export class AppModule {}With the above configuration, a particular user is limited to 10 requests within 60000 milliseconds. To make it easier to read, we can use built-in helpers such as seconds() or minutes() to specify the Time To Live (TTL) parameter.
1import { Module } from '@nestjs/common';
2import { ThrottlerModule, seconds } from '@nestjs/throttler';
3
4@Module({
5 imports: [
6 ThrottlerModule.forRoot({
7 throttlers: [
8 {
9 ttl: seconds(60),
10 limit: 10,
11 },
12 ],
13 }),
14 // ...
15 ],
16})
17export class AppModule {}Applying the Throttler
To apply the Throttler, we need to use the ThrottlerGuard class provided by NestJS. In NestJS, guards determine whether or not a given request should be allowed. The most common way of applying the ThrottlerGuard is to use it globally by adding a provider.
1import { Module } from '@nestjs/common';
2import { seconds, ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
3import { APP_GUARD } from '@nestjs/core';
4
5@Module({
6 imports: [
7 ThrottlerModule.forRoot({
8 throttlers: [
9 {
10 ttl: seconds(60),
11 limit: 10,
12 },
13 ],
14 }),
15 // ...
16 ],
17 providers: [
18 {
19 provide: APP_GUARD,
20 useClass: ThrottlerGuard,
21 },
22 ],
23})
24export class AppModule {}Thanks to the above, if we have more than 10 requests coming from a particular IP in 60 seconds, they will be blocked. Instead of the desired response, the API will respond with the status code “429 Too Many Requests”.
Customizing the behavior
Even if the guard is applied globally, we can change its behavior by turning off the rate limiting for a particular controller.
1import {
2 Body,
3 Controller,
4 Get,
5 Param,
6 ParseIntPipe,
7 Post,
8 Req,
9 UseGuards,
10} from '@nestjs/common';
11import { ArticlesService } from './articles.service';
12import { CreateArticleDto } from './dto/create-article.dto';
13import { JwtAuthenticationGuard } from '../authentication/jwt-authentication.guard';
14import { RequestWithUser } from '../authentication/request-with-user.interface';
15import { SkipThrottle } from '@nestjs/throttler';
16
17@SkipThrottle()
18@Controller('articles')
19export class ArticlesController {
20 constructor(private readonly articlesService: ArticlesService) {}
21
22 // This route is rate limited
23 @Get()
24 @SkipThrottle({ default: false })
25 getAll() {
26 return this.articlesService.getAll();
27 }
28
29 // This route is NOT rate limited
30 @Get(':id')
31 getById(@Param('id', ParseIntPipe) id: number) {
32 return this.articlesService.getById(id);
33 }
34
35 // This route is NOT rate limited
36 @Post()
37 @UseGuards(JwtAuthenticationGuard)
38 create(@Body() article: CreateArticleDto, @Req() request: RequestWithUser) {
39 return this.articlesService.create(article, request.user.id);
40 }
41
42 // ...
43}We can also override the Throttler settings we set up globally using the @Throttle() decorator.
1import { Controller } from '@nestjs/common';
2import { seconds, Throttle } from '@nestjs/throttler';
3
4@Throttle({ default: { limit: 4, ttl: seconds(30) } })
5@Controller('articles')
6export class ArticlesController {
7 // ...
8}Dealing with proxies
If our application runs behind a proxy server, HTTP requests from various users share the same IP from the perspective of our NestJS application. This prevents the Throttler from working correctly.
Fortunately, we can identify the IP address of a client connecting through a proxy thanks to the X-Forwarded-For header. To use it, we need to turn on the trust proxy setting.
1import { NestFactory } from '@nestjs/core';
2import { AppModule } from './app.module';
3import { NestExpressApplication } from '@nestjs/platform-express';
4
5async function bootstrap() {
6 const app = await NestFactory.create<NestExpressApplication>(AppModule);
7
8 app.set('trust proxy', 'loopback');
9
10 await app.listen(3000);
11}
12bootstrap();Specifying the configuration per environment
There is a good chance that we would like to use different values in different environments. We can do that by specifying environment variables.
1export interface EnvironmentVariables {
2 THROTTLER_TTL_SECONDS: string;
3 THROTTLER_LIMIT: string;
4 // ...
5}1THROTTLER_TTL_SECONDS=60
2THROTTLER_LIMIT=101import { Module } from '@nestjs/common';
2import { ConfigModule, ConfigService } from '@nestjs/config';
3import * as Joi from 'joi';
4import { ThrottlerModule, seconds } from '@nestjs/throttler';
5import { EnvironmentVariables } from './utilities/environment-variables';
6
7@Module({
8 imports: [
9 ConfigModule.forRoot({
10 validationSchema: Joi.object({
11 THROTTLER_TTL_SECONDS: Joi.number().required(),
12 THROTTLER_LIMIT: Joi.number().required(),
13 // ...
14 }),
15 }),
16 ThrottlerModule.forRootAsync({
17 imports: [ConfigModule],
18 inject: [ConfigService],
19 useFactory: (config: ConfigService<EnvironmentVariables, true>) => [
20 {
21 ttl: seconds(config.get('THROTTLER_TTL_SECONDS')),
22 limit: config.get('THROTTLER_LIMIT'),
23 },
24 ],
25 }),
26 // ...
27 ],
28})
29export class AppModule {}Working with multiple app instances
By default, the Throttler module keeps track of the requests in the application’s memory. This works fine as long as only one instance of our application exists. However, if we have multiple instances of our NestJS app, we should create a shared cache to store the information about requests using Redis.
Fortunately, we can use the @nest-lab/throttler-storage-redis library to use a Redis storage shared by multiple instances of our NestJS application using Throttler.
If you want to know moure about caching with Redis, check out API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
Summary
In this article, we explained why our REST API might need a rate limiter. To implement it, we used the Throttler module developed by the NestJS team. We also learned how to change the Throttler configuration per environment. Besides that, we also know how to deal with web proxies and multiple application instances. All of that gives us a solid understanding of how rate limiting with Throttler works and how to use it.