te.js 2.0.3 → 2.1.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/README.md +197 -187
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -0
- package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
- package/auto-docs/{llm → docs-llm}/provider.js +132 -187
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -489
- package/docs/auto-docs.md +216 -215
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -233
- package/docs/database.md +390 -391
- package/docs/error-handling.md +438 -417
- package/docs/file-uploads.md +333 -334
- package/docs/getting-started.md +214 -215
- package/docs/middleware.md +355 -356
- package/docs/rate-limiting.md +393 -394
- package/docs/routing.md +302 -302
- package/package.json +62 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -356
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -0
- package/server/errors/llm-error-service.js +140 -0
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -113
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -363
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -0
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
- package/auto-docs/llm/index.js +0 -6
- package/auto-docs/llm/parse.js +0 -88
package/docs/rate-limiting.md
CHANGED
|
@@ -1,394 +1,393 @@
|
|
|
1
|
-
# Rate Limiting
|
|
2
|
-
|
|
3
|
-
Tejas includes a powerful rate limiting system to protect your API from abuse. It supports multiple algorithms and storage backends.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```javascript
|
|
8
|
-
import Tejas from 'te.js';
|
|
9
|
-
|
|
10
|
-
const app = new Tejas();
|
|
11
|
-
|
|
12
|
-
app
|
|
13
|
-
.withRedis({ url: 'redis://localhost:6379' })
|
|
14
|
-
.withRateLimit({
|
|
15
|
-
maxRequests: 100,
|
|
16
|
-
timeWindowSeconds: 60
|
|
17
|
-
})
|
|
18
|
-
.takeoff();
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
This limits all endpoints to 100 requests per minute per IP address.
|
|
22
|
-
|
|
23
|
-
## Configuration Options
|
|
24
|
-
|
|
25
|
-
```javascript
|
|
26
|
-
app.withRateLimit({
|
|
27
|
-
// Core settings
|
|
28
|
-
maxRequests: 100, // Maximum requests in time window
|
|
29
|
-
timeWindowSeconds: 60, // Time window in seconds
|
|
30
|
-
|
|
31
|
-
// Algorithm selection
|
|
32
|
-
algorithm: 'sliding-window', // 'sliding-window' | 'token-bucket' | 'fixed-window'
|
|
33
|
-
|
|
34
|
-
// Storage backend
|
|
35
|
-
store: 'redis', // 'redis' | 'memory'
|
|
36
|
-
|
|
37
|
-
// Custom key generator (defaults to IP-based)
|
|
38
|
-
keyGenerator: (ammo) => ammo.ip,
|
|
39
|
-
|
|
40
|
-
// Algorithm-specific options
|
|
41
|
-
algorithmOptions: {},
|
|
42
|
-
|
|
43
|
-
// Key prefix for storage keys (useful for namespacing)
|
|
44
|
-
keyPrefix: 'rl:',
|
|
45
|
-
|
|
46
|
-
// Header format
|
|
47
|
-
headerFormat: {
|
|
48
|
-
type: 'standard', // 'standard' | 'legacy' | 'both'
|
|
49
|
-
draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
|
|
50
|
-
draft8: false // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
// Custom handler when rate limited
|
|
54
|
-
onRateLimited: (ammo) => {
|
|
55
|
-
ammo.fire(429, { error: 'Slow down!' });
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Algorithms
|
|
61
|
-
|
|
62
|
-
### Sliding Window (Default)
|
|
63
|
-
|
|
64
|
-
Best for smooth, accurate rate limiting. Prevents the "burst at window boundary" problem.
|
|
65
|
-
|
|
66
|
-
```javascript
|
|
67
|
-
app.withRateLimit({
|
|
68
|
-
maxRequests: 100,
|
|
69
|
-
timeWindowSeconds: 60,
|
|
70
|
-
algorithm: 'sliding-window'
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**How it works:** Calculates the request rate using a weighted combination of the current and previous time windows.
|
|
75
|
-
|
|
76
|
-
### Token Bucket
|
|
77
|
-
|
|
78
|
-
Best for APIs that allow occasional bursts while maintaining a long-term average rate.
|
|
79
|
-
|
|
80
|
-
```javascript
|
|
81
|
-
app.withRateLimit({
|
|
82
|
-
maxRequests: 100,
|
|
83
|
-
timeWindowSeconds: 60,
|
|
84
|
-
algorithm: 'token-bucket',
|
|
85
|
-
algorithmOptions: {
|
|
86
|
-
refillRate: 1.67, // Tokens per second (100/60)
|
|
87
|
-
burstSize: 150 // Maximum tokens (allows 50% burst)
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**How it works:** Tokens are added at a steady rate. Each request consumes a token. Allows bursts up to `burstSize`.
|
|
93
|
-
|
|
94
|
-
### Fixed Window
|
|
95
|
-
|
|
96
|
-
Simplest algorithm. Good for basic use cases.
|
|
97
|
-
|
|
98
|
-
```javascript
|
|
99
|
-
app.withRateLimit({
|
|
100
|
-
maxRequests: 100,
|
|
101
|
-
timeWindowSeconds: 60,
|
|
102
|
-
algorithm: 'fixed-window',
|
|
103
|
-
algorithmOptions: {
|
|
104
|
-
strictWindow: true // Align to clock boundaries
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
**How it works:** Counts requests in fixed time windows (e.g., every minute). Can allow 2x requests at window boundaries.
|
|
110
|
-
|
|
111
|
-
## Storage Backends
|
|
112
|
-
|
|
113
|
-
### Memory (Default)
|
|
114
|
-
|
|
115
|
-
Good for single-server deployments:
|
|
116
|
-
|
|
117
|
-
```javascript
|
|
118
|
-
app.withRateLimit({
|
|
119
|
-
maxRequests: 100,
|
|
120
|
-
timeWindowSeconds: 60,
|
|
121
|
-
store: 'memory'
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Pros:** No external dependencies, fast
|
|
126
|
-
**Cons:** Not shared between server instances
|
|
127
|
-
|
|
128
|
-
### Redis
|
|
129
|
-
|
|
130
|
-
Required for distributed/multi-server deployments:
|
|
131
|
-
|
|
132
|
-
```javascript
|
|
133
|
-
app
|
|
134
|
-
.withRedis({ url: 'redis://localhost:6379' })
|
|
135
|
-
.withRateLimit({
|
|
136
|
-
maxRequests: 100,
|
|
137
|
-
timeWindowSeconds: 60,
|
|
138
|
-
store: 'redis'
|
|
139
|
-
});
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**Pros:** Shared across all servers, persistent
|
|
143
|
-
**Cons:** Requires Redis server, slightly higher latency
|
|
144
|
-
|
|
145
|
-
> **Important:** Initialize Redis with `withRedis()` before using `store: 'redis'`
|
|
146
|
-
|
|
147
|
-
## Custom Key Generation
|
|
148
|
-
|
|
149
|
-
By default, rate limiting is based on client IP. Customize this:
|
|
150
|
-
|
|
151
|
-
### By User ID
|
|
152
|
-
|
|
153
|
-
```javascript
|
|
154
|
-
app.withRateLimit({
|
|
155
|
-
maxRequests: 100,
|
|
156
|
-
timeWindowSeconds: 60,
|
|
157
|
-
keyGenerator: (ammo) => ammo.user?.id || ammo.ip
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### By API Key
|
|
162
|
-
|
|
163
|
-
```javascript
|
|
164
|
-
app.withRateLimit({
|
|
165
|
-
maxRequests: 1000,
|
|
166
|
-
timeWindowSeconds: 60,
|
|
167
|
-
keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### By Endpoint
|
|
172
|
-
|
|
173
|
-
```javascript
|
|
174
|
-
app.withRateLimit({
|
|
175
|
-
maxRequests: 100,
|
|
176
|
-
timeWindowSeconds: 60,
|
|
177
|
-
keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}`
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Response Headers
|
|
182
|
-
|
|
183
|
-
Rate limit headers are automatically added to responses:
|
|
184
|
-
|
|
185
|
-
### Standard Headers (Default)
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
RateLimit-Limit: 100
|
|
189
|
-
RateLimit-Remaining: 95
|
|
190
|
-
RateLimit-Reset: 1706540400
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### Legacy Headers
|
|
194
|
-
|
|
195
|
-
```javascript
|
|
196
|
-
app.withRateLimit({
|
|
197
|
-
headerFormat: { type: 'legacy' }
|
|
198
|
-
});
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
X-RateLimit-Limit: 100
|
|
203
|
-
X-RateLimit-Remaining: 95
|
|
204
|
-
X-RateLimit-Reset: 1706540400
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Both Header Types
|
|
208
|
-
|
|
209
|
-
```javascript
|
|
210
|
-
app.withRateLimit({
|
|
211
|
-
headerFormat: { type: 'both' }
|
|
212
|
-
});
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Draft 7 Policy Header
|
|
216
|
-
|
|
217
|
-
```javascript
|
|
218
|
-
app.withRateLimit({
|
|
219
|
-
headerFormat: { type: 'standard', draft7: true }
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Adds: `RateLimit-Policy: 100;w=60`
|
|
224
|
-
|
|
225
|
-
## When Rate Limited
|
|
226
|
-
|
|
227
|
-
### Default Behavior
|
|
228
|
-
|
|
229
|
-
Returns `429 Too Many Requests` with a `Retry-After` header (seconds until the rate limit resets).
|
|
230
|
-
|
|
231
|
-
### Custom Handler
|
|
232
|
-
|
|
233
|
-
```javascript
|
|
234
|
-
app.withRateLimit({
|
|
235
|
-
maxRequests: 100,
|
|
236
|
-
timeWindowSeconds: 60,
|
|
237
|
-
onRateLimited: (ammo) => {
|
|
238
|
-
ammo.fire(429, {
|
|
239
|
-
error: 'Rate limit exceeded',
|
|
240
|
-
message: 'Please slow down and try again later',
|
|
241
|
-
retryAfter: ammo.res.getHeader('Retry-After')
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## Route-Specific Rate Limiting
|
|
248
|
-
|
|
249
|
-
Apply different limits to different routes using middleware:
|
|
250
|
-
|
|
251
|
-
```javascript
|
|
252
|
-
import Tejas, { Target } from 'te.js';
|
|
253
|
-
import rateLimiter from 'te.js/rate-limit/index.js';
|
|
254
|
-
|
|
255
|
-
const app = new Tejas();
|
|
256
|
-
const api = new Target('/api');
|
|
257
|
-
|
|
258
|
-
// Strict limit for auth endpoints
|
|
259
|
-
const authLimiter = rateLimiter({
|
|
260
|
-
maxRequests: 5,
|
|
261
|
-
timeWindowSeconds: 60,
|
|
262
|
-
algorithm: 'fixed-window'
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Relaxed limit for read operations
|
|
266
|
-
const readLimiter = rateLimiter({
|
|
267
|
-
maxRequests: 1000,
|
|
268
|
-
timeWindowSeconds: 60,
|
|
269
|
-
algorithm: 'sliding-window'
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// Apply to specific routes
|
|
273
|
-
api.register('/login', authLimiter, (ammo) => {
|
|
274
|
-
// Login logic
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
api.register('/data', readLimiter, (ammo) => {
|
|
278
|
-
// Data retrieval
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## Algorithm Comparison
|
|
283
|
-
|
|
284
|
-
| Algorithm | Best For | Burst Handling | Accuracy | Memory |
|
|
285
|
-
|-----------|----------|----------------|----------|--------|
|
|
286
|
-
| **Sliding Window** | Most APIs | Smooth | High | Medium |
|
|
287
|
-
| **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
|
|
288
|
-
| **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
|
|
289
|
-
|
|
290
|
-
## Examples
|
|
291
|
-
|
|
292
|
-
### API with Different Tiers
|
|
293
|
-
|
|
294
|
-
```javascript
|
|
295
|
-
const tierLimits = {
|
|
296
|
-
free: { maxRequests: 100, timeWindowSeconds: 3600 },
|
|
297
|
-
pro: { maxRequests: 1000, timeWindowSeconds: 3600 },
|
|
298
|
-
enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 }
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
app.withRateLimit({
|
|
302
|
-
...tierLimits.free, // Default to free tier
|
|
303
|
-
keyGenerator: (ammo) => {
|
|
304
|
-
const tier = ammo.user?.tier || 'free';
|
|
305
|
-
return `${tier}:${ammo.user?.id || ammo.ip}`;
|
|
306
|
-
},
|
|
307
|
-
algorithmOptions: {
|
|
308
|
-
// Dynamically set limits based on tier
|
|
309
|
-
getLimits: (key) => {
|
|
310
|
-
const tier = key.split(':')[0];
|
|
311
|
-
return tierLimits[tier] || tierLimits.free;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Endpoint-Specific with Global Fallback
|
|
318
|
-
|
|
319
|
-
```javascript
|
|
320
|
-
// Global rate limit
|
|
321
|
-
app.withRateLimit({
|
|
322
|
-
maxRequests: 1000,
|
|
323
|
-
timeWindowSeconds: 60
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// Stricter limit for expensive endpoints
|
|
327
|
-
const expensiveLimiter = rateLimiter({
|
|
328
|
-
maxRequests: 10,
|
|
329
|
-
timeWindowSeconds: 60,
|
|
330
|
-
store: 'redis'
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
api.register('/search', expensiveLimiter, (ammo) => {
|
|
334
|
-
// Search logic
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
api.register('/export', expensiveLimiter, (ammo) => {
|
|
338
|
-
// Export logic
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
## Monitoring
|
|
343
|
-
|
|
344
|
-
Check rate limit status in your handlers:
|
|
345
|
-
|
|
346
|
-
```javascript
|
|
347
|
-
target.register('/status', (ammo) => {
|
|
348
|
-
ammo.fire({
|
|
349
|
-
limit: ammo.res.getHeader('RateLimit-Limit'),
|
|
350
|
-
remaining: ammo.res.getHeader('RateLimit-Remaining'),
|
|
351
|
-
reset: ammo.res.getHeader('RateLimit-Reset')
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
## Custom Storage Backend
|
|
357
|
-
|
|
358
|
-
Extend the `RateLimitStorage` base class to create your own storage backend:
|
|
359
|
-
|
|
360
|
-
```javascript
|
|
361
|
-
import RateLimitStorage from 'te.js/rate-limit/storage/base.js';
|
|
362
|
-
|
|
363
|
-
class PostgresStorage extends RateLimitStorage {
|
|
364
|
-
async get(key) {
|
|
365
|
-
// Retrieve rate limit data for key
|
|
366
|
-
// Return object or null if not found
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async set(key, value, ttl) {
|
|
370
|
-
// Store rate limit data with TTL (seconds)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async increment(key) {
|
|
374
|
-
// Increment counter, return new value or null
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async delete(key) {
|
|
378
|
-
// Delete rate limit data for key
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
The built-in backends (`MemoryStorage` and `RedisStorage`) both extend this base class.
|
|
384
|
-
|
|
385
|
-
## Best Practices
|
|
386
|
-
|
|
387
|
-
1. **Use Redis in production** — Memory store doesn't scale across instances
|
|
388
|
-
2. **Set appropriate limits** — Too strict frustrates users, too lenient invites abuse
|
|
389
|
-
3. **Different limits for different endpoints** — Auth endpoints need stricter limits
|
|
390
|
-
4. **Include headers** — Help clients self-regulate
|
|
391
|
-
5. **Provide clear error messages** — Tell users when they can retry
|
|
392
|
-
6. **Consider user tiers** — Premium users may need higher limits
|
|
393
|
-
7. **Monitor and adjust** — Track rate limit hits and adjust accordingly
|
|
394
|
-
|
|
1
|
+
# Rate Limiting
|
|
2
|
+
|
|
3
|
+
Tejas includes a powerful rate limiting system to protect your API from abuse. It supports multiple algorithms and storage backends.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import Tejas from 'te.js';
|
|
9
|
+
|
|
10
|
+
const app = new Tejas();
|
|
11
|
+
|
|
12
|
+
app
|
|
13
|
+
.withRedis({ url: 'redis://localhost:6379' })
|
|
14
|
+
.withRateLimit({
|
|
15
|
+
maxRequests: 100,
|
|
16
|
+
timeWindowSeconds: 60
|
|
17
|
+
})
|
|
18
|
+
.takeoff();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This limits all endpoints to 100 requests per minute per IP address.
|
|
22
|
+
|
|
23
|
+
## Configuration Options
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
app.withRateLimit({
|
|
27
|
+
// Core settings
|
|
28
|
+
maxRequests: 100, // Maximum requests in time window
|
|
29
|
+
timeWindowSeconds: 60, // Time window in seconds
|
|
30
|
+
|
|
31
|
+
// Algorithm selection
|
|
32
|
+
algorithm: 'sliding-window', // 'sliding-window' | 'token-bucket' | 'fixed-window'
|
|
33
|
+
|
|
34
|
+
// Storage backend
|
|
35
|
+
store: 'redis', // 'redis' | 'memory'
|
|
36
|
+
|
|
37
|
+
// Custom key generator (defaults to IP-based)
|
|
38
|
+
keyGenerator: (ammo) => ammo.ip,
|
|
39
|
+
|
|
40
|
+
// Algorithm-specific options
|
|
41
|
+
algorithmOptions: {},
|
|
42
|
+
|
|
43
|
+
// Key prefix for storage keys (useful for namespacing)
|
|
44
|
+
keyPrefix: 'rl:',
|
|
45
|
+
|
|
46
|
+
// Header format
|
|
47
|
+
headerFormat: {
|
|
48
|
+
type: 'standard', // 'standard' | 'legacy' | 'both'
|
|
49
|
+
draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
|
|
50
|
+
draft8: false // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Custom handler when rate limited
|
|
54
|
+
onRateLimited: (ammo) => {
|
|
55
|
+
ammo.fire(429, { error: 'Slow down!' });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Algorithms
|
|
61
|
+
|
|
62
|
+
### Sliding Window (Default)
|
|
63
|
+
|
|
64
|
+
Best for smooth, accurate rate limiting. Prevents the "burst at window boundary" problem.
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
app.withRateLimit({
|
|
68
|
+
maxRequests: 100,
|
|
69
|
+
timeWindowSeconds: 60,
|
|
70
|
+
algorithm: 'sliding-window'
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**How it works:** Calculates the request rate using a weighted combination of the current and previous time windows.
|
|
75
|
+
|
|
76
|
+
### Token Bucket
|
|
77
|
+
|
|
78
|
+
Best for APIs that allow occasional bursts while maintaining a long-term average rate.
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
app.withRateLimit({
|
|
82
|
+
maxRequests: 100,
|
|
83
|
+
timeWindowSeconds: 60,
|
|
84
|
+
algorithm: 'token-bucket',
|
|
85
|
+
algorithmOptions: {
|
|
86
|
+
refillRate: 1.67, // Tokens per second (100/60)
|
|
87
|
+
burstSize: 150 // Maximum tokens (allows 50% burst)
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**How it works:** Tokens are added at a steady rate. Each request consumes a token. Allows bursts up to `burstSize`.
|
|
93
|
+
|
|
94
|
+
### Fixed Window
|
|
95
|
+
|
|
96
|
+
Simplest algorithm. Good for basic use cases.
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
app.withRateLimit({
|
|
100
|
+
maxRequests: 100,
|
|
101
|
+
timeWindowSeconds: 60,
|
|
102
|
+
algorithm: 'fixed-window',
|
|
103
|
+
algorithmOptions: {
|
|
104
|
+
strictWindow: true // Align to clock boundaries
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**How it works:** Counts requests in fixed time windows (e.g., every minute). Can allow 2x requests at window boundaries.
|
|
110
|
+
|
|
111
|
+
## Storage Backends
|
|
112
|
+
|
|
113
|
+
### Memory (Default)
|
|
114
|
+
|
|
115
|
+
Good for single-server deployments:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
app.withRateLimit({
|
|
119
|
+
maxRequests: 100,
|
|
120
|
+
timeWindowSeconds: 60,
|
|
121
|
+
store: 'memory'
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Pros:** No external dependencies, fast
|
|
126
|
+
**Cons:** Not shared between server instances
|
|
127
|
+
|
|
128
|
+
### Redis
|
|
129
|
+
|
|
130
|
+
Required for distributed/multi-server deployments:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
app
|
|
134
|
+
.withRedis({ url: 'redis://localhost:6379' })
|
|
135
|
+
.withRateLimit({
|
|
136
|
+
maxRequests: 100,
|
|
137
|
+
timeWindowSeconds: 60,
|
|
138
|
+
store: 'redis'
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Pros:** Shared across all servers, persistent
|
|
143
|
+
**Cons:** Requires Redis server, slightly higher latency
|
|
144
|
+
|
|
145
|
+
> **Important:** Initialize Redis with `withRedis()` before using `store: 'redis'`
|
|
146
|
+
|
|
147
|
+
## Custom Key Generation
|
|
148
|
+
|
|
149
|
+
By default, rate limiting is based on client IP. Customize this:
|
|
150
|
+
|
|
151
|
+
### By User ID
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
app.withRateLimit({
|
|
155
|
+
maxRequests: 100,
|
|
156
|
+
timeWindowSeconds: 60,
|
|
157
|
+
keyGenerator: (ammo) => ammo.user?.id || ammo.ip
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### By API Key
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
app.withRateLimit({
|
|
165
|
+
maxRequests: 1000,
|
|
166
|
+
timeWindowSeconds: 60,
|
|
167
|
+
keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### By Endpoint
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
app.withRateLimit({
|
|
175
|
+
maxRequests: 100,
|
|
176
|
+
timeWindowSeconds: 60,
|
|
177
|
+
keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}`
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Response Headers
|
|
182
|
+
|
|
183
|
+
Rate limit headers are automatically added to responses:
|
|
184
|
+
|
|
185
|
+
### Standard Headers (Default)
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
RateLimit-Limit: 100
|
|
189
|
+
RateLimit-Remaining: 95
|
|
190
|
+
RateLimit-Reset: 1706540400
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Legacy Headers
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
app.withRateLimit({
|
|
197
|
+
headerFormat: { type: 'legacy' }
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
X-RateLimit-Limit: 100
|
|
203
|
+
X-RateLimit-Remaining: 95
|
|
204
|
+
X-RateLimit-Reset: 1706540400
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Both Header Types
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
app.withRateLimit({
|
|
211
|
+
headerFormat: { type: 'both' }
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Draft 7 Policy Header
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
app.withRateLimit({
|
|
219
|
+
headerFormat: { type: 'standard', draft7: true }
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Adds: `RateLimit-Policy: 100;w=60`
|
|
224
|
+
|
|
225
|
+
## When Rate Limited
|
|
226
|
+
|
|
227
|
+
### Default Behavior
|
|
228
|
+
|
|
229
|
+
Returns `429 Too Many Requests` with a `Retry-After` header (seconds until the rate limit resets).
|
|
230
|
+
|
|
231
|
+
### Custom Handler
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
app.withRateLimit({
|
|
235
|
+
maxRequests: 100,
|
|
236
|
+
timeWindowSeconds: 60,
|
|
237
|
+
onRateLimited: (ammo) => {
|
|
238
|
+
ammo.fire(429, {
|
|
239
|
+
error: 'Rate limit exceeded',
|
|
240
|
+
message: 'Please slow down and try again later',
|
|
241
|
+
retryAfter: ammo.res.getHeader('Retry-After')
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Route-Specific Rate Limiting
|
|
248
|
+
|
|
249
|
+
Apply different limits to different routes using middleware:
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
import Tejas, { Target } from 'te.js';
|
|
253
|
+
import rateLimiter from 'te.js/rate-limit/index.js';
|
|
254
|
+
|
|
255
|
+
const app = new Tejas();
|
|
256
|
+
const api = new Target('/api');
|
|
257
|
+
|
|
258
|
+
// Strict limit for auth endpoints
|
|
259
|
+
const authLimiter = rateLimiter({
|
|
260
|
+
maxRequests: 5,
|
|
261
|
+
timeWindowSeconds: 60,
|
|
262
|
+
algorithm: 'fixed-window'
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Relaxed limit for read operations
|
|
266
|
+
const readLimiter = rateLimiter({
|
|
267
|
+
maxRequests: 1000,
|
|
268
|
+
timeWindowSeconds: 60,
|
|
269
|
+
algorithm: 'sliding-window'
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Apply to specific routes
|
|
273
|
+
api.register('/login', authLimiter, (ammo) => {
|
|
274
|
+
// Login logic
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
api.register('/data', readLimiter, (ammo) => {
|
|
278
|
+
// Data retrieval
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Algorithm Comparison
|
|
283
|
+
|
|
284
|
+
| Algorithm | Best For | Burst Handling | Accuracy | Memory |
|
|
285
|
+
|-----------|----------|----------------|----------|--------|
|
|
286
|
+
| **Sliding Window** | Most APIs | Smooth | High | Medium |
|
|
287
|
+
| **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
|
|
288
|
+
| **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
|
|
289
|
+
|
|
290
|
+
## Examples
|
|
291
|
+
|
|
292
|
+
### API with Different Tiers
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
const tierLimits = {
|
|
296
|
+
free: { maxRequests: 100, timeWindowSeconds: 3600 },
|
|
297
|
+
pro: { maxRequests: 1000, timeWindowSeconds: 3600 },
|
|
298
|
+
enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 }
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
app.withRateLimit({
|
|
302
|
+
...tierLimits.free, // Default to free tier
|
|
303
|
+
keyGenerator: (ammo) => {
|
|
304
|
+
const tier = ammo.user?.tier || 'free';
|
|
305
|
+
return `${tier}:${ammo.user?.id || ammo.ip}`;
|
|
306
|
+
},
|
|
307
|
+
algorithmOptions: {
|
|
308
|
+
// Dynamically set limits based on tier
|
|
309
|
+
getLimits: (key) => {
|
|
310
|
+
const tier = key.split(':')[0];
|
|
311
|
+
return tierLimits[tier] || tierLimits.free;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Endpoint-Specific with Global Fallback
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
// Global rate limit
|
|
321
|
+
app.withRateLimit({
|
|
322
|
+
maxRequests: 1000,
|
|
323
|
+
timeWindowSeconds: 60
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Stricter limit for expensive endpoints
|
|
327
|
+
const expensiveLimiter = rateLimiter({
|
|
328
|
+
maxRequests: 10,
|
|
329
|
+
timeWindowSeconds: 60,
|
|
330
|
+
store: 'redis'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
api.register('/search', expensiveLimiter, (ammo) => {
|
|
334
|
+
// Search logic
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
api.register('/export', expensiveLimiter, (ammo) => {
|
|
338
|
+
// Export logic
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Monitoring
|
|
343
|
+
|
|
344
|
+
Check rate limit status in your handlers:
|
|
345
|
+
|
|
346
|
+
```javascript
|
|
347
|
+
target.register('/status', (ammo) => {
|
|
348
|
+
ammo.fire({
|
|
349
|
+
limit: ammo.res.getHeader('RateLimit-Limit'),
|
|
350
|
+
remaining: ammo.res.getHeader('RateLimit-Remaining'),
|
|
351
|
+
reset: ammo.res.getHeader('RateLimit-Reset')
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Custom Storage Backend
|
|
357
|
+
|
|
358
|
+
Extend the `RateLimitStorage` base class to create your own storage backend:
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
import RateLimitStorage from 'te.js/rate-limit/storage/base.js';
|
|
362
|
+
|
|
363
|
+
class PostgresStorage extends RateLimitStorage {
|
|
364
|
+
async get(key) {
|
|
365
|
+
// Retrieve rate limit data for key
|
|
366
|
+
// Return object or null if not found
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async set(key, value, ttl) {
|
|
370
|
+
// Store rate limit data with TTL (seconds)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async increment(key) {
|
|
374
|
+
// Increment counter, return new value or null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async delete(key) {
|
|
378
|
+
// Delete rate limit data for key
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The built-in backends (`MemoryStorage` and `RedisStorage`) both extend this base class.
|
|
384
|
+
|
|
385
|
+
## Best Practices
|
|
386
|
+
|
|
387
|
+
1. **Use Redis in production** — Memory store doesn't scale across instances
|
|
388
|
+
2. **Set appropriate limits** — Too strict frustrates users, too lenient invites abuse
|
|
389
|
+
3. **Different limits for different endpoints** — Auth endpoints need stricter limits
|
|
390
|
+
4. **Include headers** — Help clients self-regulate
|
|
391
|
+
5. **Provide clear error messages** — Tell users when they can retry
|
|
392
|
+
6. **Consider user tiers** — Premium users may need higher limits
|
|
393
|
+
7. **Monitor and adjust** — Track rate limit hits and adjust accordingly
|