@kyrasource/rate-limit
v1.0.1
Published
Storage-agnostic rate limiting utilities for NestJS applications with in-memory store by default and pluggable backend support
Downloads
16
Maintainers
Readme
@kyrasource/rate-limit
Storage-agnostic rate limiting utilities for NestJS applications. Ships with a fast in-memory store by default and a clean store interface so you can plug in Redis or any other backend without coupling.
Features
- ✅ Flexible Storage: In-memory by default, swap to Redis or any custom backend
- ✅ Global Guard: Automatically rate limits all endpoints (configurable)
- ✅ Per-Route Configuration: Override limits on specific handlers
- ✅ IP-Based Tracking: Extracts client IP from headers and socket
- ✅ Whitelist Support: Bypass rate limits for trusted IPs
- ✅ Standard Headers: Returns
X-RateLimit-*andRetry-Afterheaders - ✅ TypeScript Support: Full type safety with interfaces
- ✅ Error Handling: Returns HTTP 429 when limit exceeded
- ✅ Production Ready: Used in distributed deployments with Redis
Installation
npm install @kyrasource/rate-limitQuick Start
1. Setup in Your Module
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RateLimitModule } from '@kyrasource/rate-limit';
import rateLimitConfig from './config/rate-limit.config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [rateLimitConfig],
}),
RateLimitModule, // Registers as APP_GUARD automatically
],
})
export class AppModule {}2. Configure Rate Limits in .env
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
RATE_LIMIT_PREFIX=rate-limit
RATE_LIMIT_MESSAGE=Too many requests, please try again later.
RATE_LIMIT_WHITELIST=127.0.0.1,::13. Apply Decorators to Routes
import { Controller, Get } from '@nestjs/common';
import { RateLimit } from '@kyrasource/rate-limit';
@Controller('api')
export class ApiController {
// Uses global rate limit (100 requests per 15 minutes)
@Get('public')
public() {
return { message: 'public endpoint' };
}
// Custom: 30 requests per minute
@Get('search')
@RateLimit({ windowMs: 60_000, maxRequests: 30 })
search() {
return { results: [] };
}
// Custom: 5 requests per hour
@Get('expensive')
@RateLimit({ windowMs: 3_600_000, maxRequests: 5 })
expensiveOperation() {
return { data: 'heavy computation' };
}
// No rate limiting
@Get('health')
@RateLimit({ enabled: false })
health() {
return { status: 'ok' };
}
}Configuration
Create src/config/rate-limit.config.ts:
import { registerAs } from '@nestjs/config';
import { RateLimitConfig } from '@kyrasource/rate-limit';
export default registerAs('rateLimitConfig', (): RateLimitConfig => ({
enabled: process.env.RATE_LIMIT_ENABLED !== 'false',
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '900000', 10), // 15 min
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? '100', 10),
keyPrefix: process.env.RATE_LIMIT_PREFIX ?? 'rate-limit',
message: process.env.RATE_LIMIT_MESSAGE ?? 'Too many requests',
whitelist: (process.env.RATE_LIMIT_WHITELIST ?? '')
.split(',')
.map(ip => ip.trim())
.filter(Boolean),
}));API Reference
RateLimit Decorator
Override global settings per route:
@RateLimit({
enabled: boolean; // Enable/disable limiting for this route
windowMs: number; // Time window in milliseconds
maxRequests: number; // Max requests allowed in window
keyPrefix?: string; // Redis key prefix (optional)
message?: string; // Custom error message (optional)
whitelist?: string[]; // IPs to bypass limits (optional)
})HTTP Headers
Every response includes:
X-RateLimit-Limit: 100 // Max requests in window
X-RateLimit-Remaining: 95 // Requests left
X-RateLimit-Reset: 1705718200 // UNIX timestamp when window resets
Retry-After: 30 // (Only when 429) Seconds to retryError Response (HTTP 429)
When rate limit is exceeded:
{
"statusCode": 429,
"message": "Too many requests, please try again later",
"retryAfter": 42
}Advanced Usage
Using Redis Store (Distributed)
For multi-instance deployments, use Redis:
// src/shared/rate-limit/redis-rate-limit.store.ts
import { Injectable } from '@nestjs/common';
import { RateLimitStore } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis';
@Injectable()
export class RedisRateLimitStore implements RateLimitStore {
constructor(private readonly redis: RedisService) {}
async get<T = any>(key: string): Promise<T | null> {
return this.redis.get<T>(key);
}
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
await this.redis.set(key, value, ttlSeconds);
}
}
// In your module:
import { RateLimitModule, RATE_LIMIT_STORE } from '@kyrasource/rate-limit';
@Module({
imports: [RateLimitModule],
providers: [
RedisService,
RedisRateLimitStore,
{
provide: RATE_LIMIT_STORE,
useClass: RedisRateLimitStore,
},
],
})
export class ApiModule {}Whitelisting Trusted IPs
@Controller('admin')
export class AdminController {
// Admin endpoints - stricter limits, but whitelist internal IPs
@Get('users')
@RateLimit({
windowMs: 60_000,
maxRequests: 10,
whitelist: ['127.0.0.1', '192.168.1.0/24', '::1'],
})
listUsers() {
return { users: [] };
}
}Disabling for Specific Routes
@Controller('health')
export class HealthController {
// No rate limiting for health checks
@Get()
@RateLimit({ enabled: false })
check() {
return { status: 'healthy' };
}
}Custom Key Strategy
By default, keys are built as: ${keyPrefix}:${method}:${path}:${clientIp}
This means:
- Each HTTP method is tracked separately
- Each route is tracked separately
- Each client IP is tracked separately
Example: rate-limit:POST:/api/search:203.0.113.45
IP Detection
The guard extracts client IP in this order:
x-forwarded-forheader (supports multiple IPs, takes first)x-real-ipheaderrequest.ipfrom Expressrequest.socket.remoteAddress- Falls back to
'unknown'
Exports
From @kyrasource/rate-limit you can import:
export { RateLimitModule } from './rate-limit.module';
export { RateLimitGuard } from './rate-limit.guard';
export { RateLimitService } from './rate-limit.service';
export { RateLimit } from './rate-limit.decorator';
export { RATE_LIMIT_STORE } from './rate-limit.constants';
export { RateLimitStore } from './rate-limit.store';
export { RateLimitConfig, RateLimitOptions, RateLimitResult } from './rate-limit.types';Default Configuration
If not configured via environment or ConfigService:
{
enabled: true,
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 100,
keyPrefix: 'ratelimit',
message: 'Too many requests, please try again later',
whitelist: [],
}Troubleshooting
"Rate limit exceeded" immediately
- Check
RATE_LIMIT_MAX_REQUESTSvalue (too low?) - Check
RATE_LIMIT_WINDOW_MSvalue (too short?) - Verify IP detection is working (check logs for client IP)
Whitelist not working
- Ensure IPs are comma-separated in
.env - Check that client IP matches whitelist entry
- Note: X-Forwarded-For may have multiple IPs
Store errors
- If using Redis store, verify Redis is running
- Check
RATE_LIMIT_STOREprovider is registered - Monitor logs for connection errors
License
MIT - See LICENSE file
Support
For issues, feature requests, or documentation, visit:
- GitHub: kyrasource/rate-limit
- npm: @kyrasource/rate-limit enabled: true, windowMs: 15 * 60 * 1000, // 15 minutes maxRequests: 100, keyPrefix: 'ratelimit', message: 'Too many requests, please try again later', whitelist: [] as string[], };
Provide your own in `AppModule` (optional):
```ts
import { Module } from '@nestjs/common';
import { RateLimitModule } from '@kushi/rate-limit-lib';
@Module({
imports: [RateLimitModule],
providers: [
{ provide: 'RATE_LIMIT_CONFIG', useValue: {
enabled: true,
windowMs: 10_000,
maxRequests: 20,
keyPrefix: 'api',
message: 'Rate limit exceeded',
whitelist: ['127.0.0.1'],
} },
],
})
export class AppModule {}Storage Abstraction (Replaceable Store)
This package is storage-agnostic. It relies on a minimal interface and a DI token so you can swap implementations.
- Token:
RATE_LIMIT_STORE - Interface:
export interface RateLimitStore {
get<T = any>(key: string): Promise<T | null>;
set(key: string, value: any, ttlSeconds?: number): Promise<void>;
}It ships with a default InMemoryRateLimitStore. For multi-instance or distributed environments, override the token with your own store (e.g., Redis).
Example: Redis-backed store (adapter in your app)
// src/shared/rate-limit/redis-rate-limit.store.ts
import { Injectable } from '@nestjs/common';
import { RateLimitStore } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis'; // your Redis client wrapper
@Injectable()
export class RedisRateLimitStore implements RateLimitStore {
constructor(private readonly redis: RedisService) {}
async get<T = any>(key: string): Promise<T | null> {
return this.redis.get<T>(key);
}
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
await this.redis.set(key, value, ttlSeconds);
}
}Register it to override the default store:
import { Module } from '@nestjs/common';
import { RateLimitModule, RATE_LIMIT_STORE } from '@kyrasource/rate-limit';
import { RedisService } from '@kyrasource/redis';
import { RedisRateLimitStore } from './shared/rate-limit/redis-rate-limit.store';
@Module({
imports: [RateLimitModule],
providers: [
RedisService,
{ provide: RATE_LIMIT_STORE, useClass: RedisRateLimitStore },
],
})
export class AppModule {}Per-Route Options
Use the RateLimit decorator to override any of the following per handler/class:
export interface RateLimitOptions {
enabled: boolean;
windowMs: number; // duration of the window in ms
maxRequests: number; // max requests allowed in window
keyPrefix: string; // prefix for storage keys
message: string; // error message when blocked
whitelist?: string[]; // list of IPs to bypass limiting
}Headers and Errors
On every request, the guard sets the following headers:
X-RateLimit-Limit: the configured limit for the windowX-RateLimit-Remaining: remaining requests in the current windowX-RateLimit-Reset: UNIX seconds when the window resetsRetry-After: seconds until reset (only when blocked)
When blocked, the guard throws HTTP 429 with a body like:
{
"message": "Too many requests, please try again later",
"retryAfter": 42
}Exports
From @kyrasource/rate-limit you can import:
RateLimitModuleRateLimitGuardRateLimitServiceRateLimit(decorator)RATE_LIMIT_STORE(DI token)InMemoryRateLimitStoreandRateLimitStore(for custom adapters)- Types:
RateLimitOptions,RateLimitResult,RateLimitState
Notes
- The in-memory store is great for single-instance setups and tests. For distributed/clustered deployments, use a shared store (e.g., Redis) by overriding
RATE_LIMIT_STORE. - Keys are built as:
${keyPrefix}:${method}:${normalizedPath}:${ip}wherenormalizedPathstrips query strings.
Build
cd packages/rate-limit-lib
npm run buildThis compiles to dist/ with type declarations.
