nextlimiter 1.0.2 → 1.0.4
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/README.md +482 -438
- package/package.json +27 -5
- package/src/adapters/fastify.js +80 -0
- package/src/adapters/hono.js +87 -0
- package/src/adapters/next.js +157 -0
- package/types/index.d.ts +49 -0
package/README.md
CHANGED
|
@@ -1,438 +1,482 @@
|
|
|
1
|
-
# NextLimiter
|
|
2
|
-
|
|
3
|
-
**Production-ready rate limiting for Node.js — simple, smart, and built for real SaaS apps.**
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/nextlimiter)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
[]()
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Why NextLimiter?
|
|
12
|
-
|
|
13
|
-
Most rate limiting libraries make you choose between simple-but-limited and powerful-but-complex. NextLimiter does both.
|
|
14
|
-
|
|
15
|
-
| Feature | express-rate-limit | rate-limiter-flexible | **NextLimiter** |
|
|
16
|
-
|---|---|---|---|
|
|
17
|
-
| Zero-config usage | ✓ | ✗ | ✓ |
|
|
18
|
-
| SaaS plan tiers | ✗ | ✗ | **✓** |
|
|
19
|
-
| Smart / behavior-based limiting | ✗ | ✗ | **✓** |
|
|
20
|
-
| Built-in analytics | ✗ | ✗ | **✓** |
|
|
21
|
-
| Programmatic `check()` API | ✗ | ✓ | ✓ |
|
|
22
|
-
| Named presets | ✗ | ✗ | **✓** |
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
#### `
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
createLimiter({ strategy: '
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
#### `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```js
|
|
87
|
-
createLimiter({ strategy: '
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
//
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**By
|
|
243
|
-
```js
|
|
244
|
-
createLimiter({ keyBy: '
|
|
245
|
-
// Reads
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
|
339
|
-
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1
|
+
# NextLimiter
|
|
2
|
+
|
|
3
|
+
**Production-ready rate limiting for Node.js — simple, smart, and built for real SaaS apps.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/nextlimiter)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/abhishekck31/nexlimiter/actions)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why NextLimiter?
|
|
12
|
+
|
|
13
|
+
Most rate limiting libraries make you choose between simple-but-limited and powerful-but-complex. NextLimiter does both.
|
|
14
|
+
|
|
15
|
+
| Feature | express-rate-limit | rate-limiter-flexible | **NextLimiter** |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| Zero-config usage | ✓ | ✗ | ✓ |
|
|
18
|
+
| SaaS plan tiers | ✗ | ✗ | **✓** |
|
|
19
|
+
| Smart / behavior-based limiting | ✗ | ✗ | **✓** |
|
|
20
|
+
| Built-in analytics | ✗ | ✗ | **✓** |
|
|
21
|
+
| Programmatic `check()` API | ✗ | ✓ | ✓ |
|
|
22
|
+
| Named presets | ✗ | ✗ | **✓** |
|
|
23
|
+
| Redis support (built-in) | ✗ | ✓ | **✓** |
|
|
24
|
+
| TypeScript types included | ✓ | ✓ | ✓ |
|
|
25
|
+
| Zero dependencies | ✓ | ✗ | ✓ |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install nextlimiter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
No Redis required. Works out of the box with in-memory storage.
|
|
36
|
+
|
|
37
|
+
For distributed / multi-server deployments, add Redis:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install ioredis
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### Zero-config (one line)
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const { autoLimit } = require('nextlimiter');
|
|
51
|
+
app.use(autoLimit());
|
|
52
|
+
// → 100 requests/min per IP, sliding window, no setup needed
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Custom configuration
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const { createLimiter } = require('nextlimiter');
|
|
59
|
+
|
|
60
|
+
const limiter = createLimiter({
|
|
61
|
+
windowMs: 60_000, // 1 minute
|
|
62
|
+
max: 100, // max 100 requests per window
|
|
63
|
+
strategy: 'sliding-window',
|
|
64
|
+
logging: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
app.use('/api', limiter.middleware());
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Core Concepts
|
|
73
|
+
|
|
74
|
+
### Strategies
|
|
75
|
+
|
|
76
|
+
#### `sliding-window` (default)
|
|
77
|
+
The most accurate algorithm. Uses a weighted two-window approximation — same approach as Cloudflare and Nginx's `limit_req_zone`. No boundary-burst problem. O(1) memory per key.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
createLimiter({ strategy: 'sliding-window', windowMs: 60_000, max: 100 })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `token-bucket`
|
|
84
|
+
Tokens refill continuously. Allows controlled bursts up to `max` tokens while enforcing a sustained rate. Used by Stripe for their API. Best for APIs where occasional spikes are expected.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
createLimiter({ strategy: 'token-bucket', windowMs: 60_000, max: 100 })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### `fixed-window`
|
|
91
|
+
Simplest approach. Counts requests in fixed time intervals. Lowest memory usage. Note: susceptible to boundary-burst attacks (a client can use 2× the limit by straddling a window boundary).
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
createLimiter({ strategy: 'fixed-window', windowMs: 60_000, max: 100 })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Features
|
|
100
|
+
|
|
101
|
+
### SaaS Plan-Based Limiting
|
|
102
|
+
|
|
103
|
+
Apply different rate limits based on subscription tier without writing conditional logic:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const { createPlanLimiter } = require('nextlimiter');
|
|
107
|
+
|
|
108
|
+
// Built-in plans: free (60/min), pro (600/min), enterprise (6000/min)
|
|
109
|
+
const limiter = createPlanLimiter('pro', {
|
|
110
|
+
keyBy: 'api-key',
|
|
111
|
+
logging: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.use('/api', limiter.middleware());
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Custom plan definitions:**
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const limiter = createLimiter({
|
|
121
|
+
plans: {
|
|
122
|
+
startup: { windowMs: 60_000, max: 150, burstMax: 20 },
|
|
123
|
+
growth: { windowMs: 60_000, max: 500, burstMax: 80 },
|
|
124
|
+
enterprise: { windowMs: 60_000, max: 5000, burstMax: 500 },
|
|
125
|
+
},
|
|
126
|
+
plan: 'startup', // swap this based on req.user.plan at runtime
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Dynamic plan selection per request:**
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// Create a limiter per plan, pick the right one in your route handler
|
|
134
|
+
const planLimiters = {
|
|
135
|
+
free: createPlanLimiter('free', { keyBy: 'api-key' }),
|
|
136
|
+
pro: createPlanLimiter('pro', { keyBy: 'api-key' }),
|
|
137
|
+
enterprise: createPlanLimiter('enterprise', { keyBy: 'api-key' }),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
app.use('/api', (req, res, next) => {
|
|
141
|
+
const plan = req.user?.plan || 'free';
|
|
142
|
+
return planLimiters[plan].middleware()(req, res, next);
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Smart Rate Limiting
|
|
149
|
+
|
|
150
|
+
Detects burst traffic and dynamically reduces limits for suspicious clients — without blocking them entirely.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const limiter = createLimiter({
|
|
154
|
+
windowMs: 60_000,
|
|
155
|
+
max: 100,
|
|
156
|
+
smart: true,
|
|
157
|
+
smartThreshold: 2.0, // flag if rate exceeds 2× normal
|
|
158
|
+
smartCooldownMs: 60_000, // penalty lasts 60 seconds
|
|
159
|
+
smartPenaltyFactor: 0.5, // reduce limit to 50% during penalty
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**How it works:**
|
|
164
|
+
1. Tracks the request rate for each key in a short observation window (10% of `windowMs`)
|
|
165
|
+
2. If rate exceeds `normalRate × smartThreshold`, the key is flagged
|
|
166
|
+
3. Flagged keys get `floor(max × smartPenaltyFactor)` as their effective limit
|
|
167
|
+
4. Penalty expires after `smartCooldownMs`
|
|
168
|
+
5. Legitimate users are completely unaffected
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Named Presets
|
|
173
|
+
|
|
174
|
+
Four built-in presets for the most common scenarios:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
const { createPresetLimiter } = require('nextlimiter');
|
|
178
|
+
|
|
179
|
+
// Strict — 30 req/min, sliding window, smart limiting on
|
|
180
|
+
app.use('/admin', createPresetLimiter('strict').middleware());
|
|
181
|
+
|
|
182
|
+
// Relaxed — 300 req/min, token bucket
|
|
183
|
+
app.use('/public', createPresetLimiter('relaxed').middleware());
|
|
184
|
+
|
|
185
|
+
// API — 100 req/min, api-key based, smart on
|
|
186
|
+
app.use('/api', createPresetLimiter('api').middleware());
|
|
187
|
+
|
|
188
|
+
// Auth — 10 attempts per 15 minutes (brute-force protection)
|
|
189
|
+
app.post('/login', createPresetLimiter('auth').middleware());
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Built-in Analytics
|
|
195
|
+
|
|
196
|
+
Every limiter instance tracks metrics automatically:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const stats = limiter.getStats();
|
|
200
|
+
|
|
201
|
+
console.log(stats);
|
|
202
|
+
// {
|
|
203
|
+
// totalRequests: 15420,
|
|
204
|
+
// blockedRequests: 234,
|
|
205
|
+
// allowedRequests: 15186,
|
|
206
|
+
// blockRate: 0.0152,
|
|
207
|
+
// topKeys: [
|
|
208
|
+
// { key: 'nextlimiter:ip:1.2.3.4', count: 892 },
|
|
209
|
+
// { key: 'nextlimiter:ip:5.6.7.8', count: 441 },
|
|
210
|
+
// ],
|
|
211
|
+
// topBlocked: [
|
|
212
|
+
// { key: 'nextlimiter:ip:1.2.3.4', count: 78 },
|
|
213
|
+
// ],
|
|
214
|
+
// trackedSince: '2024-01-15T10:00:00.000Z',
|
|
215
|
+
// uptimeMs: 3600000,
|
|
216
|
+
// config: {
|
|
217
|
+
// strategy: 'sliding-window',
|
|
218
|
+
// windowMs: 60000,
|
|
219
|
+
// max: 100,
|
|
220
|
+
// plan: 'pro',
|
|
221
|
+
// smart: true,
|
|
222
|
+
// }
|
|
223
|
+
// }
|
|
224
|
+
|
|
225
|
+
// Expose as an endpoint (protect with auth in production)
|
|
226
|
+
app.get('/admin/stats', (req, res) => res.json(limiter.getStats()));
|
|
227
|
+
|
|
228
|
+
// Reset counters
|
|
229
|
+
limiter.resetStats();
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### Key-Based Limiting
|
|
235
|
+
|
|
236
|
+
**By IP (default):**
|
|
237
|
+
```js
|
|
238
|
+
createLimiter({ keyBy: 'ip' })
|
|
239
|
+
// Uses X-Forwarded-For → X-Real-IP → req.ip (proxy-aware)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**By authenticated user ID:**
|
|
243
|
+
```js
|
|
244
|
+
createLimiter({ keyBy: 'user-id' })
|
|
245
|
+
// Reads req.user.id → req.user._id → req.userId
|
|
246
|
+
// Falls back to IP for unauthenticated requests
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**By API key:**
|
|
250
|
+
```js
|
|
251
|
+
createLimiter({ keyBy: 'api-key' })
|
|
252
|
+
// Reads Authorization: Bearer <token> → X-API-Key header → ?apiKey query param
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Custom key function:**
|
|
256
|
+
```js
|
|
257
|
+
createLimiter({
|
|
258
|
+
keyGenerator: (req) => `tenant:${req.headers['x-tenant-id']}`,
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Programmatic API
|
|
265
|
+
|
|
266
|
+
Use `limiter.check()` for rate limiting outside HTTP middleware — WebSockets, background jobs, cron tasks:
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
const limiter = createLimiter({ windowMs: 60_000, max: 10 });
|
|
270
|
+
|
|
271
|
+
// WebSocket message handler
|
|
272
|
+
async function onMessage(userId, message) {
|
|
273
|
+
const result = await limiter.check(`ws:${userId}`);
|
|
274
|
+
|
|
275
|
+
if (!result.allowed) {
|
|
276
|
+
socket.emit('error', {
|
|
277
|
+
message: 'Rate limit exceeded',
|
|
278
|
+
retryAfter: result.retryAfter,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
processMessage(message);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Background job
|
|
287
|
+
async function runExport(userId) {
|
|
288
|
+
const result = await limiter.check(`export:${userId}`);
|
|
289
|
+
if (!result.allowed) throw new Error(`Try again in ${result.retryAfter}s`);
|
|
290
|
+
// ... run export
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### Developer-Friendly Logging
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
createLimiter({ logging: true, logPrefix: '[API]' })
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Output:
|
|
303
|
+
```
|
|
304
|
+
2024-01-15T10:23:41.000Z [API] BLOCKED ip:1.2.3.4 (101/100) via sliding-window
|
|
305
|
+
2024-01-15T10:23:42.000Z [API] BLOCKED ip:1.2.3.4 (45/22) via sliding-window [smart]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Colors are automatically disabled in non-TTY environments (CI, Docker logs).
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### Skip and Custom Handlers
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
createLimiter({
|
|
316
|
+
// Skip rate limiting for specific requests
|
|
317
|
+
skip: (req) =>
|
|
318
|
+
req.path === '/health' ||
|
|
319
|
+
req.headers['x-internal-service'] === 'true' ||
|
|
320
|
+
req.ip === '127.0.0.1',
|
|
321
|
+
|
|
322
|
+
// Full control over the blocked response
|
|
323
|
+
onLimitReached: (req, res, result) => {
|
|
324
|
+
res.status(429).json({
|
|
325
|
+
error: 'Rate limit exceeded',
|
|
326
|
+
retryAfter: result.retryAfter,
|
|
327
|
+
upgrade: 'Upgrade to Pro for 10× the rate limit',
|
|
328
|
+
docsUrl: 'https://yourapp.com/docs/rate-limits',
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Full Configuration Reference
|
|
337
|
+
|
|
338
|
+
| Option | Type | Default | Description |
|
|
339
|
+
|---|---|---|---|
|
|
340
|
+
| `windowMs` | `number` | `60000` | Time window in milliseconds |
|
|
341
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
342
|
+
| `strategy` | `string` | `'sliding-window'` | `'fixed-window'` \| `'sliding-window'` \| `'token-bucket'` |
|
|
343
|
+
| `keyBy` | `string\|fn` | `'ip'` | `'ip'` \| `'user-id'` \| `'api-key'` \| `(req) => string` |
|
|
344
|
+
| `keyPrefix` | `string` | `'nextlimiter:'` | Redis/store key prefix |
|
|
345
|
+
| `plan` | `string` | `null` | `'free'` \| `'pro'` \| `'enterprise'` |
|
|
346
|
+
| `plans` | `object` | built-in | Custom plan definitions |
|
|
347
|
+
| `preset` | `string` | `null` | `'strict'` \| `'relaxed'` \| `'api'` \| `'auth'` |
|
|
348
|
+
| `smart` | `boolean` | `false` | Enable smart burst detection |
|
|
349
|
+
| `smartThreshold` | `number` | `2.0` | Rate multiplier that triggers penalty |
|
|
350
|
+
| `smartCooldownMs` | `number` | `60000` | How long smart penalty lasts |
|
|
351
|
+
| `smartPenaltyFactor` | `number` | `0.5` | Limit multiplier during penalty (0–1) |
|
|
352
|
+
| `logging` | `boolean` | `false` | Enable console logging |
|
|
353
|
+
| `logPrefix` | `string` | `'[NextLimiter]'` | Log line prefix |
|
|
354
|
+
| `headers` | `boolean` | `true` | Send `X-RateLimit-*` headers |
|
|
355
|
+
| `statusCode` | `number` | `429` | HTTP status for blocked requests |
|
|
356
|
+
| `message` | `string` | `'Too many requests...'` | Default 429 message |
|
|
357
|
+
| `store` | `Store` | `MemoryStore` | Custom storage backend (`MemoryStore` or `RedisStore`) |
|
|
358
|
+
| `skip` | `fn` | `null` | `(req) => boolean` — skip rate limiting |
|
|
359
|
+
| `onLimitReached` | `fn` | `null` | `(req, res, result) => void` |
|
|
360
|
+
| `keyGenerator` | `fn` | `null` | `(req) => string` — override key generation |
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Response Headers
|
|
365
|
+
|
|
366
|
+
Every response includes these headers:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
X-RateLimit-Limit: 100
|
|
370
|
+
X-RateLimit-Remaining: 43
|
|
371
|
+
X-RateLimit-Reset: 1705315200
|
|
372
|
+
X-RateLimit-Strategy: sliding-window
|
|
373
|
+
Retry-After: 47 ← only on 429 responses
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Redis Support (Distributed Deployments)
|
|
379
|
+
|
|
380
|
+
NextLimiter ships a built-in `RedisStore` for distributed / multi-server setups. It uses an **atomic Lua script** for `increment()` so there are zero race conditions across multiple Node.js processes behind a load balancer.
|
|
381
|
+
|
|
382
|
+
### Installation
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
npm install ioredis
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Usage
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
const Redis = require('ioredis');
|
|
392
|
+
const { createLimiter, RedisStore } = require('nextlimiter');
|
|
393
|
+
|
|
394
|
+
const redis = new Redis(); // connects to 127.0.0.1:6379 by default
|
|
395
|
+
|
|
396
|
+
const limiter = createLimiter({
|
|
397
|
+
store: new RedisStore(redis),
|
|
398
|
+
max: 100,
|
|
399
|
+
windowMs: 60_000,
|
|
400
|
+
strategy: 'sliding-window',
|
|
401
|
+
keyBy: 'ip',
|
|
402
|
+
logging: true,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
app.use('/api', limiter.middleware());
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### With Redis Cluster / Sentinel
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
// Redis Cluster
|
|
412
|
+
const redis = new Redis.Cluster([{ host: '127.0.0.1', port: 6380 }]);
|
|
413
|
+
|
|
414
|
+
// Redis Sentinel
|
|
415
|
+
const redis = new Redis({
|
|
416
|
+
sentinels: [{ host: 'sentinel-1', port: 26379 }],
|
|
417
|
+
name: 'mymaster',
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const limiter = createLimiter({ store: new RedisStore(redis), max: 100 });
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### How the Lua Script Works
|
|
424
|
+
|
|
425
|
+
The `increment()` method uses a single-script atomic operation:
|
|
426
|
+
|
|
427
|
+
```lua
|
|
428
|
+
local new = redis.call('INCR', KEYS[1])
|
|
429
|
+
if new == 1 then
|
|
430
|
+
redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[1]))
|
|
431
|
+
end
|
|
432
|
+
return new
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
All three steps (INCR + conditional PEXPIRE) execute atomically in Redis — no race condition is possible, even with 100+ Node.js instances.
|
|
436
|
+
|
|
437
|
+
### Custom Store Interface
|
|
438
|
+
|
|
439
|
+
You can also implement your own store for any backend (MongoDB, DynamoDB, Postgres, etc.) — just implement 5 methods:
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
class MyCustomStore {
|
|
443
|
+
async get(key) { /* return value or undefined */ }
|
|
444
|
+
async set(key, value, ttlMs) { /* store with TTL */ }
|
|
445
|
+
async increment(key, ttlMs) { /* atomic increment, return new count */ }
|
|
446
|
+
async delete(key) { /* remove key */ }
|
|
447
|
+
keys() { /* return string[] — can be [] */ }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const limiter = createLimiter({ store: new MyCustomStore() });
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## TypeScript
|
|
456
|
+
|
|
457
|
+
Full TypeScript support included — no `@types/nextlimiter` needed:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { createLimiter, RedisStore, LimiterOptions, RateLimitResult, Store } from 'nextlimiter';
|
|
461
|
+
import Redis from 'ioredis';
|
|
462
|
+
|
|
463
|
+
// In-memory (development)
|
|
464
|
+
const limiter = createLimiter({ windowMs: 60_000, max: 100 });
|
|
465
|
+
|
|
466
|
+
// Redis-backed (production)
|
|
467
|
+
const redis = new Redis();
|
|
468
|
+
const prodLimiter = createLimiter({
|
|
469
|
+
store: new RedisStore(redis),
|
|
470
|
+
max: 100,
|
|
471
|
+
strategy: 'sliding-window',
|
|
472
|
+
smart: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const result: RateLimitResult = await prodLimiter.check('user:42');
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## License
|
|
481
|
+
|
|
482
|
+
MIT
|