nextlimiter 1.0.2 → 1.0.3

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.
Files changed (2) hide show
  1. package/README.md +482 -438
  2. package/package.json +9 -5
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
- [![npm version](https://badge.fury.io/js/nextlimiter.svg)](https://www.npmjs.com/package/nextlimiter)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
- [![Tests](https://img.shields.io/badge/tests-38%20passing-brightgreen)]()
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
- | TypeScript types included | | ✓ | |
24
- | Zero dependencies | ✓ | | ✓ |
25
-
26
- ---
27
-
28
- ## Installation
29
-
30
- ```bash
31
- npm install nextlimiter
32
- ```
33
-
34
- No Redis required. Works out of the box with in-memory storage.
35
-
36
- ---
37
-
38
- ## Quick Start
39
-
40
- ### Zero-config (one line)
41
-
42
- ```js
43
- const { autoLimit } = require('nextlimiter');
44
- app.use(autoLimit());
45
- // 100 requests/min per IP, sliding window, no setup needed
46
- ```
47
-
48
- ### Custom configuration
49
-
50
- ```js
51
- const { createLimiter } = require('nextlimiter');
52
-
53
- const limiter = createLimiter({
54
- windowMs: 60_000, // 1 minute
55
- max: 100, // max 100 requests per window
56
- strategy: 'sliding-window',
57
- logging: true,
58
- });
59
-
60
- app.use('/api', limiter.middleware());
61
- ```
62
-
63
- ---
64
-
65
- ## Core Concepts
66
-
67
- ### Strategies
68
-
69
- #### `sliding-window` (default)
70
- 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.
71
-
72
- ```js
73
- createLimiter({ strategy: 'sliding-window', windowMs: 60_000, max: 100 })
74
- ```
75
-
76
- #### `token-bucket`
77
- 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.
78
-
79
- ```js
80
- createLimiter({ strategy: 'token-bucket', windowMs: 60_000, max: 100 })
81
- ```
82
-
83
- #### `fixed-window`
84
- Simplest approach. Counts requests in fixed time intervals. Lowest memory usage. Note: susceptible to boundary-burst attacks (a client can use the limit by straddling a window boundary).
85
-
86
- ```js
87
- createLimiter({ strategy: 'fixed-window', windowMs: 60_000, max: 100 })
88
- ```
89
-
90
- ---
91
-
92
- ## Features
93
-
94
- ### SaaS Plan-Based Limiting
95
-
96
- Apply different rate limits based on subscription tier without writing conditional logic:
97
-
98
- ```js
99
- const { createPlanLimiter } = require('nextlimiter');
100
-
101
- // Built-in plans: free (60/min), pro (600/min), enterprise (6000/min)
102
- const limiter = createPlanLimiter('pro', {
103
- keyBy: 'api-key',
104
- logging: true,
105
- });
106
-
107
- app.use('/api', limiter.middleware());
108
- ```
109
-
110
- **Custom plan definitions:**
111
-
112
- ```js
113
- const limiter = createLimiter({
114
- plans: {
115
- startup: { windowMs: 60_000, max: 150, burstMax: 20 },
116
- growth: { windowMs: 60_000, max: 500, burstMax: 80 },
117
- enterprise: { windowMs: 60_000, max: 5000, burstMax: 500 },
118
- },
119
- plan: 'startup', // swap this based on req.user.plan at runtime
120
- });
121
- ```
122
-
123
- **Dynamic plan selection per request:**
124
-
125
- ```js
126
- // Create a limiter per plan, pick the right one in your route handler
127
- const planLimiters = {
128
- free: createPlanLimiter('free', { keyBy: 'api-key' }),
129
- pro: createPlanLimiter('pro', { keyBy: 'api-key' }),
130
- enterprise: createPlanLimiter('enterprise', { keyBy: 'api-key' }),
131
- };
132
-
133
- app.use('/api', (req, res, next) => {
134
- const plan = req.user?.plan || 'free';
135
- return planLimiters[plan].middleware()(req, res, next);
136
- });
137
- ```
138
-
139
- ---
140
-
141
- ### Smart Rate Limiting
142
-
143
- Detects burst traffic and dynamically reduces limits for suspicious clients — without blocking them entirely.
144
-
145
- ```js
146
- const limiter = createLimiter({
147
- windowMs: 60_000,
148
- max: 100,
149
- smart: true,
150
- smartThreshold: 2.0, // flag if rate exceeds normal
151
- smartCooldownMs: 60_000, // penalty lasts 60 seconds
152
- smartPenaltyFactor: 0.5, // reduce limit to 50% during penalty
153
- });
154
- ```
155
-
156
- **How it works:**
157
- 1. Tracks the request rate for each key in a short observation window (10% of `windowMs`)
158
- 2. If rate exceeds `normalRate × smartThreshold`, the key is flagged
159
- 3. Flagged keys get `floor(max × smartPenaltyFactor)` as their effective limit
160
- 4. Penalty expires after `smartCooldownMs`
161
- 5. Legitimate users are completely unaffected
162
-
163
- ---
164
-
165
- ### Named Presets
166
-
167
- Four built-in presets for the most common scenarios:
168
-
169
- ```js
170
- const { createPresetLimiter } = require('nextlimiter');
171
-
172
- // Strict — 30 req/min, sliding window, smart limiting on
173
- app.use('/admin', createPresetLimiter('strict').middleware());
174
-
175
- // Relaxed — 300 req/min, token bucket
176
- app.use('/public', createPresetLimiter('relaxed').middleware());
177
-
178
- // API — 100 req/min, api-key based, smart on
179
- app.use('/api', createPresetLimiter('api').middleware());
180
-
181
- // Auth — 10 attempts per 15 minutes (brute-force protection)
182
- app.post('/login', createPresetLimiter('auth').middleware());
183
- ```
184
-
185
- ---
186
-
187
- ### Built-in Analytics
188
-
189
- Every limiter instance tracks metrics automatically:
190
-
191
- ```js
192
- const stats = limiter.getStats();
193
-
194
- console.log(stats);
195
- // {
196
- // totalRequests: 15420,
197
- // blockedRequests: 234,
198
- // allowedRequests: 15186,
199
- // blockRate: 0.0152,
200
- // topKeys: [
201
- // { key: 'nextlimiter:ip:1.2.3.4', count: 892 },
202
- // { key: 'nextlimiter:ip:5.6.7.8', count: 441 },
203
- // ],
204
- // topBlocked: [
205
- // { key: 'nextlimiter:ip:1.2.3.4', count: 78 },
206
- // ],
207
- // trackedSince: '2024-01-15T10:00:00.000Z',
208
- // uptimeMs: 3600000,
209
- // config: {
210
- // strategy: 'sliding-window',
211
- // windowMs: 60000,
212
- // max: 100,
213
- // plan: 'pro',
214
- // smart: true,
215
- // }
216
- // }
217
-
218
- // Expose as an endpoint (protect with auth in production)
219
- app.get('/admin/stats', (req, res) => res.json(limiter.getStats()));
220
-
221
- // Reset counters
222
- limiter.resetStats();
223
- ```
224
-
225
- ---
226
-
227
- ### Key-Based Limiting
228
-
229
- **By IP (default):**
230
- ```js
231
- createLimiter({ keyBy: 'ip' })
232
- // Uses X-Forwarded-For → X-Real-IP → req.ip (proxy-aware)
233
- ```
234
-
235
- **By authenticated user ID:**
236
- ```js
237
- createLimiter({ keyBy: 'user-id' })
238
- // Reads req.user.id → req.user._id → req.userId
239
- // Falls back to IP for unauthenticated requests
240
- ```
241
-
242
- **By API key:**
243
- ```js
244
- createLimiter({ keyBy: 'api-key' })
245
- // Reads Authorization: Bearer <token> X-API-Key header ?apiKey query param
246
- ```
247
-
248
- **Custom key function:**
249
- ```js
250
- createLimiter({
251
- keyGenerator: (req) => `tenant:${req.headers['x-tenant-id']}`,
252
- })
253
- ```
254
-
255
- ---
256
-
257
- ### Programmatic API
258
-
259
- Use `limiter.check()` for rate limiting outside HTTP middleware — WebSockets, background jobs, cron tasks:
260
-
261
- ```js
262
- const limiter = createLimiter({ windowMs: 60_000, max: 10 });
263
-
264
- // WebSocket message handler
265
- async function onMessage(userId, message) {
266
- const result = await limiter.check(`ws:${userId}`);
267
-
268
- if (!result.allowed) {
269
- socket.emit('error', {
270
- message: 'Rate limit exceeded',
271
- retryAfter: result.retryAfter,
272
- });
273
- return;
274
- }
275
-
276
- processMessage(message);
277
- }
278
-
279
- // Background job
280
- async function runExport(userId) {
281
- const result = await limiter.check(`export:${userId}`);
282
- if (!result.allowed) throw new Error(`Try again in ${result.retryAfter}s`);
283
- // ... run export
284
- }
285
- ```
286
-
287
- ---
288
-
289
- ### Developer-Friendly Logging
290
-
291
- ```js
292
- createLimiter({ logging: true, logPrefix: '[API]' })
293
- ```
294
-
295
- Output:
296
- ```
297
- 2024-01-15T10:23:41.000Z [API] BLOCKED ip:1.2.3.4 (101/100) via sliding-window
298
- 2024-01-15T10:23:42.000Z [API] BLOCKED ip:1.2.3.4 (45/22) via sliding-window [smart]
299
- ```
300
-
301
- Colors are automatically disabled in non-TTY environments (CI, Docker logs).
302
-
303
- ---
304
-
305
- ### Skip and Custom Handlers
306
-
307
- ```js
308
- createLimiter({
309
- // Skip rate limiting for specific requests
310
- skip: (req) =>
311
- req.path === '/health' ||
312
- req.headers['x-internal-service'] === 'true' ||
313
- req.ip === '127.0.0.1',
314
-
315
- // Full control over the blocked response
316
- onLimitReached: (req, res, result) => {
317
- res.status(429).json({
318
- error: 'Rate limit exceeded',
319
- retryAfter: result.retryAfter,
320
- upgrade: 'Upgrade to Pro for 10× the rate limit',
321
- docsUrl: 'https://yourapp.com/docs/rate-limits',
322
- });
323
- },
324
- })
325
- ```
326
-
327
- ---
328
-
329
- ## Full Configuration Reference
330
-
331
- | Option | Type | Default | Description |
332
- |---|---|---|---|
333
- | `windowMs` | `number` | `60000` | Time window in milliseconds |
334
- | `max` | `number` | `100` | Max requests per window |
335
- | `strategy` | `string` | `'sliding-window'` | `'fixed-window'` \| `'sliding-window'` \| `'token-bucket'` |
336
- | `keyBy` | `string\|fn` | `'ip'` | `'ip'` \| `'user-id'` \| `'api-key'` \| `(req) => string` |
337
- | `keyPrefix` | `string` | `'nextlimiter:'` | Redis/store key prefix |
338
- | `plan` | `string` | `null` | `'free'` \| `'pro'` \| `'enterprise'` |
339
- | `plans` | `object` | built-in | Custom plan definitions |
340
- | `preset` | `string` | `null` | `'strict'` \| `'relaxed'` \| `'api'` \| `'auth'` |
341
- | `smart` | `boolean` | `false` | Enable smart burst detection |
342
- | `smartThreshold` | `number` | `2.0` | Rate multiplier that triggers penalty |
343
- | `smartCooldownMs` | `number` | `60000` | How long smart penalty lasts |
344
- | `smartPenaltyFactor` | `number` | `0.5` | Limit multiplier during penalty (0–1) |
345
- | `logging` | `boolean` | `false` | Enable console logging |
346
- | `logPrefix` | `string` | `'[NextLimiter]'` | Log line prefix |
347
- | `headers` | `boolean` | `true` | Send `X-RateLimit-*` headers |
348
- | `statusCode` | `number` | `429` | HTTP status for blocked requests |
349
- | `message` | `string` | `'Too many requests...'` | Default 429 message |
350
- | `store` | `Store` | `MemoryStore` | Custom storage backend |
351
- | `skip` | `fn` | `null` | `(req) => boolean` skip rate limiting |
352
- | `onLimitReached` | `fn` | `null` | `(req, res, result) => void` |
353
- | `keyGenerator` | `fn` | `null` | `(req) => string` — override key generation |
354
-
355
- ---
356
-
357
- ## Response Headers
358
-
359
- Every response includes these headers:
360
-
361
- ```
362
- X-RateLimit-Limit: 100
363
- X-RateLimit-Remaining: 43
364
- X-RateLimit-Reset: 1705315200
365
- X-RateLimit-Strategy: sliding-window
366
- Retry-After: 47 ← only on 429 responses
367
- ```
368
-
369
- ---
370
-
371
- ## Custom Store (Redis example)
372
-
373
- Implement the `Store` interface to use any backend:
374
-
375
- ```js
376
- const Redis = require('ioredis');
377
-
378
- class RedisStore {
379
- constructor(client) {
380
- this.client = client;
381
- }
382
-
383
- async get(key) {
384
- const val = await this.client.get(key);
385
- return val ? JSON.parse(val) : undefined;
386
- }
387
-
388
- async set(key, value, ttlMs) {
389
- await this.client.set(key, JSON.stringify(value), 'PX', ttlMs);
390
- }
391
-
392
- async increment(key, ttlMs) {
393
- const count = await this.client.incr(key);
394
- if (count === 1) await this.client.pexpire(key, ttlMs);
395
- return count;
396
- }
397
-
398
- async delete(key) {
399
- await this.client.del(key);
400
- }
401
-
402
- keys() { return []; } // optional
403
- clear() {} // optional
404
- }
405
-
406
- // Use it:
407
- const limiter = createLimiter({
408
- store: new RedisStore(new Redis()),
409
- windowMs: 60_000,
410
- max: 100,
411
- });
412
- ```
413
-
414
- ---
415
-
416
- ## TypeScript
417
-
418
- Full TypeScript support included — no `@types/nextlimiter` needed:
419
-
420
- ```ts
421
- import { createLimiter, LimiterOptions, RateLimitResult, Store } from 'nextlimiter';
422
-
423
- const options: LimiterOptions = {
424
- windowMs: 60_000,
425
- max: 100,
426
- strategy: 'token-bucket',
427
- smart: true,
428
- };
429
-
430
- const limiter = createLimiter(options);
431
- const result: RateLimitResult = await limiter.check('user:42');
432
- ```
433
-
434
- ---
435
-
436
- ## License
437
-
438
- MIT
1
+ # NextLimiter
2
+
3
+ **Production-ready rate limiting for Node.js — simple, smart, and built for real SaaS apps.**
4
+
5
+ [![npm version](https://badge.fury.io/js/nextlimiter.svg)](https://www.npmjs.com/package/nextlimiter)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
+ [![Tests](https://img.shields.io/badge/tests-38%20passing-brightgreen)](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 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.idreq.user._idreq.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextlimiter",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Production-ready rate limiting for Node.js — sliding window, token bucket, SaaS plans, smart limiting, and built-in analytics.",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -35,11 +35,15 @@
35
35
  "node": ">=14.0.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "express": ">=4.0.0"
38
+ "express": ">=4.0.0",
39
+ "ioredis": ">=4.0.0"
39
40
  },
40
41
  "peerDependenciesMeta": {
41
42
  "express": {
42
43
  "optional": true
44
+ },
45
+ "ioredis": {
46
+ "optional": true
43
47
  }
44
48
  },
45
49
  "devDependencies": {
@@ -61,10 +65,10 @@
61
65
  },
62
66
  "repository": {
63
67
  "type": "git",
64
- "url": "git+https://github.com/abhishekck31/nextlimiter.git"
68
+ "url": "git+https://github.com/abhishekck31/nexlimiter.git"
65
69
  },
66
70
  "bugs": {
67
- "url": "https://github.com/abhishekck31/nextlimiter/issues"
71
+ "url": "https://github.com/abhishekck31/nexlimiter/issues"
68
72
  },
69
- "homepage": "https://github.com/abhishekck31/nextlimiter#readme"
73
+ "homepage": "https://github.com/abhishekck31/nexlimiter#readme"
70
74
  }