nestjs-cluster-throttle 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/package.json +104 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 White Rabbit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# nestjs-cluster-throttle
|
|
2
|
+
|
|
3
|
+
Cluster-ready rate limiting module for NestJS with Redis support and multiple rate limiting strategies.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Cluster-ready** - Works seamlessly in clustered environments
|
|
8
|
+
- 🔴 **Redis support** - Distributed rate limiting with Redis
|
|
9
|
+
- 💾 **Memory storage** - In-memory storage for single-instance applications
|
|
10
|
+
- 🎯 **Flexible strategies** - Support for fixed-window, token-bucket, and sliding-window algorithms
|
|
11
|
+
- 🎨 **Decorator-based** - Easy-to-use decorators for route protection
|
|
12
|
+
- ⚙️ **Highly configurable** - Customize limits, windows, and behavior
|
|
13
|
+
- 🔒 **Type-safe** - Written in TypeScript with full type support
|
|
14
|
+
- 🛡️ **Fail-open strategy** - Gracefully handles storage failures
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install nestjs-cluster-throttle ioredis
|
|
20
|
+
# or
|
|
21
|
+
yarn add nestjs-cluster-throttle ioredis
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Basic Usage (In-Memory)
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Module } from '@nestjs/common';
|
|
30
|
+
import { RateLimitModule } from 'nestjs-cluster-throttle';
|
|
31
|
+
|
|
32
|
+
@Module({
|
|
33
|
+
imports: [
|
|
34
|
+
RateLimitModule.forRoot({
|
|
35
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
36
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
})
|
|
40
|
+
export class AppModule {}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Redis-based (Cluster Mode)
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Module } from '@nestjs/common';
|
|
47
|
+
import { RateLimitModule } from 'nestjs-cluster-throttle';
|
|
48
|
+
|
|
49
|
+
@Module({
|
|
50
|
+
imports: [
|
|
51
|
+
RateLimitModule.forRoot({
|
|
52
|
+
windowMs: 15 * 60 * 1000,
|
|
53
|
+
max: 100,
|
|
54
|
+
clusterMode: true,
|
|
55
|
+
redisOptions: {
|
|
56
|
+
host: 'localhost',
|
|
57
|
+
port: 6379,
|
|
58
|
+
password: 'your-password', // optional
|
|
59
|
+
db: 0,
|
|
60
|
+
keyPrefix: 'rate-limit:',
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
export class AppModule {}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Async Configuration
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { Module } from '@nestjs/common';
|
|
72
|
+
import { RateLimitModule } from 'nestjs-cluster-throttle';
|
|
73
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
74
|
+
|
|
75
|
+
@Module({
|
|
76
|
+
imports: [
|
|
77
|
+
RateLimitModule.forRootAsync({
|
|
78
|
+
imports: [ConfigModule],
|
|
79
|
+
useFactory: async (configService: ConfigService) => ({
|
|
80
|
+
windowMs: configService.get('RATE_LIMIT_WINDOW_MS'),
|
|
81
|
+
max: configService.get('RATE_LIMIT_MAX'),
|
|
82
|
+
clusterMode: true,
|
|
83
|
+
redisOptions: {
|
|
84
|
+
host: configService.get('REDIS_HOST'),
|
|
85
|
+
port: configService.get('REDIS_PORT'),
|
|
86
|
+
password: configService.get('REDIS_PASSWORD'),
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
inject: [ConfigService],
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
export class AppModule {}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage in Controllers
|
|
97
|
+
|
|
98
|
+
### Route-specific Rate Limiting
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Controller, Get } from '@nestjs/common';
|
|
102
|
+
import { RateLimit } from 'nestjs-cluster-throttle';
|
|
103
|
+
|
|
104
|
+
@Controller('api')
|
|
105
|
+
export class ApiController {
|
|
106
|
+
@Get('limited')
|
|
107
|
+
@RateLimit({
|
|
108
|
+
windowMs: 60 * 1000, // 1 minute
|
|
109
|
+
max: 10, // 10 requests per minute
|
|
110
|
+
message: 'Too many requests from this IP',
|
|
111
|
+
statusCode: 429,
|
|
112
|
+
})
|
|
113
|
+
limitedEndpoint() {
|
|
114
|
+
return { message: 'This endpoint is rate limited' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Get('public')
|
|
118
|
+
publicEndpoint() {
|
|
119
|
+
return { message: 'This endpoint uses global rate limits' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Skip Rate Limiting
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { Controller, Get } from '@nestjs/common';
|
|
128
|
+
import { SkipRateLimit } from 'nestjs-cluster-throttle';
|
|
129
|
+
|
|
130
|
+
@Controller('api')
|
|
131
|
+
export class ApiController {
|
|
132
|
+
@Get('health')
|
|
133
|
+
@SkipRateLimit()
|
|
134
|
+
healthCheck() {
|
|
135
|
+
return { status: 'ok' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Advanced Configuration
|
|
141
|
+
|
|
142
|
+
### Custom Key Generator
|
|
143
|
+
|
|
144
|
+
Use custom logic to generate rate limit keys:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
RateLimitModule.forRoot({
|
|
148
|
+
windowMs: 15 * 60 * 1000,
|
|
149
|
+
max: 100,
|
|
150
|
+
keyGenerator: (request) => {
|
|
151
|
+
// Rate limit by user ID instead of IP
|
|
152
|
+
return request.user?.id || request.ip;
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Skip Requests Conditionally
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
RateLimitModule.forRoot({
|
|
161
|
+
windowMs: 15 * 60 * 1000,
|
|
162
|
+
max: 100,
|
|
163
|
+
skip: (request) => {
|
|
164
|
+
// Skip rate limiting for admin users
|
|
165
|
+
return request.user?.role === 'admin';
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Custom Handler
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
RateLimitModule.forRoot({
|
|
174
|
+
windowMs: 15 * 60 * 1000,
|
|
175
|
+
max: 100,
|
|
176
|
+
handler: (request, response) => {
|
|
177
|
+
// Custom handling when rate limit is exceeded
|
|
178
|
+
response.status(429).json({
|
|
179
|
+
error: 'Rate limit exceeded',
|
|
180
|
+
retryAfter: response.getHeader('X-RateLimit-Reset'),
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Skip Successful Requests
|
|
187
|
+
|
|
188
|
+
Only count failed requests towards the rate limit:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
RateLimitModule.forRoot({
|
|
192
|
+
windowMs: 15 * 60 * 1000,
|
|
193
|
+
max: 100,
|
|
194
|
+
skipSuccessfulRequests: true,
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Rate Limiting Strategies
|
|
199
|
+
|
|
200
|
+
### Fixed Window (Default)
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
RateLimitModule.forRoot({
|
|
204
|
+
windowMs: 60 * 1000,
|
|
205
|
+
max: 100,
|
|
206
|
+
strategy: 'fixed-window',
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Token Bucket
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
RateLimitModule.forRoot({
|
|
214
|
+
windowMs: 60 * 1000,
|
|
215
|
+
max: 100,
|
|
216
|
+
strategy: 'token-bucket',
|
|
217
|
+
burstCapacity: 150, // Allow bursts up to 150 requests
|
|
218
|
+
fillRate: 100, // Refill 100 tokens per windowMs
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Sliding Window
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
RateLimitModule.forRoot({
|
|
226
|
+
windowMs: 60 * 1000,
|
|
227
|
+
max: 100,
|
|
228
|
+
strategy: 'sliding-window',
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Programmatic Usage
|
|
233
|
+
|
|
234
|
+
Inject `RateLimitService` to check rate limits programmatically:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { Injectable } from '@nestjs/common';
|
|
238
|
+
import { RateLimitService } from 'nestjs-cluster-throttle';
|
|
239
|
+
|
|
240
|
+
@Injectable()
|
|
241
|
+
export class MyService {
|
|
242
|
+
constructor(private rateLimitService: RateLimitService) {}
|
|
243
|
+
|
|
244
|
+
async checkLimit(request: any) {
|
|
245
|
+
const result = await this.rateLimitService.checkRateLimit(request, {
|
|
246
|
+
windowMs: 60 * 1000,
|
|
247
|
+
max: 10,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (!result.allowed) {
|
|
251
|
+
throw new Error('Rate limit exceeded');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async resetUserLimit(userId: string) {
|
|
258
|
+
await this.rateLimitService.resetKey(userId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async resetAllLimits() {
|
|
262
|
+
await this.rateLimitService.resetAll();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Configuration Options
|
|
268
|
+
|
|
269
|
+
| Option | Type | Default | Description |
|
|
270
|
+
|--------|------|---------|-------------|
|
|
271
|
+
| `windowMs` | number | `900000` (15 min) | Time window in milliseconds |
|
|
272
|
+
| `max` | number | `100` | Maximum number of requests per window |
|
|
273
|
+
| `message` | string | `'Too Many Requests'` | Error message when limit is exceeded |
|
|
274
|
+
| `statusCode` | number | `429` | HTTP status code when limit is exceeded |
|
|
275
|
+
| `skipSuccessfulRequests` | boolean | `false` | Skip successful requests in counting |
|
|
276
|
+
| `keyGenerator` | function | IP-based | Function to generate rate limit key |
|
|
277
|
+
| `skip` | function | `undefined` | Function to conditionally skip rate limiting |
|
|
278
|
+
| `handler` | function | `undefined` | Custom handler for rate limit exceeded |
|
|
279
|
+
| `clusterMode` | boolean | `false` | Enable Redis for cluster mode |
|
|
280
|
+
| `redisOptions` | object | `{}` | Redis connection options |
|
|
281
|
+
| `strategy` | string | `'fixed-window'` | Rate limiting strategy |
|
|
282
|
+
| `burstCapacity` | number | `undefined` | Burst capacity for token-bucket |
|
|
283
|
+
| `fillRate` | number | `undefined` | Fill rate for token-bucket |
|
|
284
|
+
|
|
285
|
+
### Redis Options
|
|
286
|
+
|
|
287
|
+
| Option | Type | Default | Description |
|
|
288
|
+
|--------|------|---------|-------------|
|
|
289
|
+
| `host` | string | `'localhost'` | Redis host |
|
|
290
|
+
| `port` | number | `6379` | Redis port |
|
|
291
|
+
| `password` | string | `undefined` | Redis password |
|
|
292
|
+
| `db` | number | `0` | Redis database number |
|
|
293
|
+
| `keyPrefix` | string | `'rate-limit:'` | Key prefix for Redis |
|
|
294
|
+
| `enableReadyCheck` | boolean | `true` | Enable ready check |
|
|
295
|
+
|
|
296
|
+
## Response Headers
|
|
297
|
+
|
|
298
|
+
When rate limiting is active, the following headers are set:
|
|
299
|
+
|
|
300
|
+
- `X-RateLimit-Limit`: Maximum requests allowed
|
|
301
|
+
- `X-RateLimit-Remaining`: Requests remaining in current window
|
|
302
|
+
- `X-RateLimit-Reset`: Unix timestamp when the rate limit resets
|
|
303
|
+
|
|
304
|
+
## Testing
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { Test } from '@nestjs/testing';
|
|
308
|
+
import { RateLimitModule, RateLimitService }
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-cluster-throttle",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Enterprise-grade rate limiting module for NestJS with Redis support, multiple strategies, and cluster mode",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.build.json",
|
|
14
|
+
"prebuild": "rimraf dist",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:cov": "jest --coverage",
|
|
18
|
+
"test:e2e": "jest --config ./test/jest-e2e.json",
|
|
19
|
+
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
|
20
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"prepare": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"nestjs",
|
|
26
|
+
"rate-limiting",
|
|
27
|
+
"rate-limiter",
|
|
28
|
+
"throttle",
|
|
29
|
+
"throttling",
|
|
30
|
+
"cluster",
|
|
31
|
+
"redis",
|
|
32
|
+
"distributed",
|
|
33
|
+
"rate-limit",
|
|
34
|
+
"guard",
|
|
35
|
+
"middleware",
|
|
36
|
+
"api-protection",
|
|
37
|
+
"ddos-protection",
|
|
38
|
+
"request-limiting",
|
|
39
|
+
"token-bucket",
|
|
40
|
+
"sliding-window",
|
|
41
|
+
"fixed-window"
|
|
42
|
+
],
|
|
43
|
+
"author": {
|
|
44
|
+
"name": "Roman Dobrynin",
|
|
45
|
+
"email": "roman.dobrynin@gmail.com"
|
|
46
|
+
},
|
|
47
|
+
"contributors": [
|
|
48
|
+
{
|
|
49
|
+
"name": "Roman Dobrynin",
|
|
50
|
+
"email": "roman.dobrynin@gmail.com"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/rdobrynin/nestjs-cluster-throttle.git"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/rdobrynin/nestjs-cluster-throttle/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/rdobrynin/nestjs-cluster-throttle#readme",
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=14.0.0",
|
|
64
|
+
"npm": ">=6.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
|
68
|
+
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
|
69
|
+
"reflect-metadata": "^0.1.13",
|
|
70
|
+
"rxjs": "^7.0.0"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"ioredis": "^5.3.2"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@nestjs/common": "^10.0.0",
|
|
77
|
+
"@nestjs/core": "^10.0.0",
|
|
78
|
+
"@nestjs/platform-express": "^10.0.0",
|
|
79
|
+
"@nestjs/testing": "^10.0.0",
|
|
80
|
+
"@types/jest": "^29.5.0",
|
|
81
|
+
"@types/node": "^20.0.0",
|
|
82
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
83
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
84
|
+
"eslint": "^8.0.0",
|
|
85
|
+
"eslint-config-prettier": "^9.0.0",
|
|
86
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
87
|
+
"jest": "^29.5.0",
|
|
88
|
+
"prettier": "^3.0.0",
|
|
89
|
+
"reflect-metadata": "^0.1.13",
|
|
90
|
+
"rimraf": "^5.0.0",
|
|
91
|
+
"rxjs": "^7.8.0",
|
|
92
|
+
"ts-jest": "^29.1.0",
|
|
93
|
+
"ts-node": "^10.9.0",
|
|
94
|
+
"typescript": "^5.0.0"
|
|
95
|
+
},
|
|
96
|
+
"publishConfig": {
|
|
97
|
+
"access": "public",
|
|
98
|
+
"registry": "https://registry.npmjs.org/"
|
|
99
|
+
},
|
|
100
|
+
"np": {
|
|
101
|
+
"yarn": false,
|
|
102
|
+
"contents": "dist"
|
|
103
|
+
}
|
|
104
|
+
}
|