nextlimiter 1.0.0 → 1.0.2

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 CHANGED
@@ -1,438 +1,438 @@
1
- # NexLimit
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/nexlimit.svg)](https://www.npmjs.com/package/nexlimit)
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 NexLimit?
12
-
13
- Most rate limiting libraries make you choose between simple-but-limited and powerful-but-complex. NexLimit does both.
14
-
15
- | Feature | express-rate-limit | rate-limiter-flexible | **NexLimit** |
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 nexlimit
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('nexlimit');
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('nexlimit');
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 2× 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('nexlimit');
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 2× 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('nexlimit');
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: 'nexlimit:ip:1.2.3.4', count: 892 },
202
- // { key: 'nexlimit:ip:5.6.7.8', count: 441 },
203
- // ],
204
- // topBlocked: [
205
- // { key: 'nexlimit: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` | `'nexlimit:'` | 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` | `'[NexLimit]'` | 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/nexlimit` needed:
419
-
420
- ```ts
421
- import { createLimiter, LimiterOptions, RateLimitResult, Store } from 'nexlimit';
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)]()
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 2× 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 2× 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