DEV Community

Mohin Sheikh
Mohin Sheikh

Posted on

Comprehensive Guide to Implementing Rate Limiting in NestJS: IP-Based and Device ID-Based Strategies.

Image description

📚 1. Introduction

Rate limiting is a crucial security and performance enhancement feature that protects your backend services from malicious or accidental overuse. In NestJS, we can implement rate limiting both on IP-based requests and Device ID-based requests.

This guide provides a step-by-step approach to implementing both methods, along with a clean folder structure and explanations of key components.


⚙️ 2. Prerequisites

Make sure you have the following installed:

  • Node.js (>= 14.x)
  • NestJS CLI (npm install -g @nestjs/cli)
  • Basic understanding of NestJS, TypeScript, and Middleware concepts

🛠️ 3. Project Setup

3.1. Create a NestJS Project

nest new rate-limiting-app
cd rate-limiting-app
Enter fullscreen mode Exit fullscreen mode

3.2. Install Required Dependencies

npm install @nestjs/throttler dotenv
Enter fullscreen mode Exit fullscreen mode

📂 4. Folder Structure

Organize your project like this:

src/
│
├── app.module.ts
├── app.controller.ts
├── main.ts
│
├── device/
│   ├── guards/
│   │   └── custom-throttler.guard.ts
│   ├── services/
│   │   └── rate-limiter.service.ts
│   ├── device.controller.ts
│
├── util/
│   └── response.util.ts
│
└── custom-throttler.guard.ts
Enter fullscreen mode Exit fullscreen mode

🌍 5. Environment Variables

Create a .env file at the root level:

PORT=3000

# Rate Limit Configurations
TTL=60000   # Time-to-live in milliseconds (60 seconds)
LIMIT=3     # Maximum requests allowed in the time window
Enter fullscreen mode Exit fullscreen mode

🚀 6. Application Entry Point

6.1. main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

🌐 7. IP-Based Rate Limiting

7.1. app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { DeviceController } from './device/device.controller';
import { ThrottlerModule } from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
import { RateLimiterService } from './device/services/rate-limiter.service';
import { CustomThrottlerGuard } from './custom-throttler.guard';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        ttl: 60,
        limit: 3,
      },
    ]),
  ],
  controllers: [AppController, DeviceController],
  providers: [
    RateLimiterService,
    {
      provide: APP_GUARD,
      useClass: CustomThrottlerGuard,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

7.2. custom-throttler.guard.ts

import {
    ThrottlerGuard as BaseThrottlerGuard,
    ThrottlerException,
    ThrottlerRequest,
} from '@nestjs/throttler';
import { Response } from 'express';
import { ResponseUtil } from './util/response.util';
import { Injectable } from '@nestjs/common';

@Injectable()
export class CustomThrottlerGuard extends BaseThrottlerGuard {
    async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
        try {
            return await super.handleRequest(requestProps);
        } catch (error) {
            if (error instanceof ThrottlerException) {
                const response = requestProps.context.switchToHttp().getResponse<Response>();
                response.status(429).json(
                    ResponseUtil.error(429, 'Too many requests from this IP, please try again later.'),
                );
            }
            throw error;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

7.3. app.controller.ts

import 'dotenv/config';
import { Controller, Get } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { ResponseUtil } from './util/response.util';

@Controller('/api/ip')
export class AppController {
  @Get('/test')
  @Throttle({ default: { limit: Number(process.env.LIMIT), ttl: Number(process.env.TTL) } })
  testIpEndpoint() {
    return ResponseUtil.success(200, 'Request successful!', {
      info: 'IP-based rate limiting is working as expected.',
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

📱 8. Device ID-Based Rate Limiting

8.1. rate-limiter.service.ts

import 'dotenv/config';
import { Injectable } from '@nestjs/common';

@Injectable()
export class RateLimiterService {
    private readonly requestCache: Map<string, { count: number; timestamp: number }> = new Map();
    private readonly limit = Number(process.env.LIMIT);
    private readonly ttl = Number(process.env.TTL);

    async isRateLimited(deviceId: string): Promise<boolean> {
        const currentTime = Date.now();
        const cachedData = this.requestCache.get(deviceId);

        if (cachedData) {
            const timeDiff = currentTime - cachedData.timestamp;
            if (timeDiff < this.ttl) {
                if (cachedData.count >= this.limit) {
                    return true;
                } else {
                    cachedData.count++;
                    return false;
                }
            } else {
                this.requestCache.set(deviceId, { count: 1, timestamp: currentTime });
                return false;
            }
        } else {
            this.requestCache.set(deviceId, { count: 1, timestamp: currentTime });
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

8.2. custom-throttler.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { RateLimiterService } from '../services/rate-limiter.service';
import { ResponseUtil } from '../../util/response.util';

@Injectable()
export class RateLimiterGuard implements CanActivate {
    constructor(private readonly rateLimiterService: RateLimiterService) {}

    async canActivate(context: ExecutionContext): Promise<boolean> {
        const request = context.switchToHttp().getRequest();
        const response = context.switchToHttp().getResponse();
        const deviceId = request.headers['device-id'];

        if (!deviceId) {
            response.status(403).json(ResponseUtil.error(403, 'Device ID is required'));
            return false;
        }

        const isRateLimited = await this.rateLimiterService.isRateLimited(deviceId);
        if (isRateLimited) {
            response.status(429).json(ResponseUtil.error(429, 'Too many requests from this device, please try again later.'));
            return false;
        }

        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

8.3. device.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RateLimiterGuard } from './guards/custom-throttler.guard';
import { ResponseUtil } from '../util/response.util';

@Controller('/api/device')
export class DeviceController {
    @Get('/test')
    @UseGuards(RateLimiterGuard)
    testDeviceEndpoint() {
        return ResponseUtil.success(200, 'Request successful!', {
            info: 'Device ID-based rate limiting is working as expected.',
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

📝 9. Advantages of Rate Limiting

  • 🚫 Prevents Abuse
  • Improved Performance
  • 🔒 Enhanced Security
  • 📊 Fair Usage Policy
  • 💸 Cost Management

10. Conclusion

This guide outlined both IP-based and Device ID-based rate-limiting strategies in NestJS.

Run the server:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Test Endpoints:

  • By IP: curl http://localhost:3000/api/ip/test
  • By Device ID: curl http://localhost:3000/api/device/test -H 'device-id: unique-device-id'

Author: Mohin Sheikh

Follow me on GitHub for more insights!

Top comments (1)

Collapse
 
test_1a07615047d9aa1b6342 profile image
test

❤️‍🔥✌️