@bantai-dev/with-rate-limit
v1.2.0
Published
Rate limiting extension for @bantai-dev/core
Maintainers
Readme
@bantai-dev/with-rate-limit
Rate limiting extension for @bantai-dev/core
Add rate limiting capabilities to your Bantai contexts with support for multiple rate limiting strategies including fixed-window, sliding-window, and token-bucket algorithms.
Website: https://bantai.vercel.app/
Installation
npm install @bantai-dev/with-rate-limit @bantai-dev/core @bantai-dev/with-storage zod
# or
pnpm add @bantai-dev/with-rate-limit @bantai-dev/core @bantai-dev/with-storage zod
# or
yarn add @bantai-dev/with-rate-limit @bantai-dev/core @bantai-dev/with-storage zodNote: @bantai-dev/core, @bantai-dev/with-storage, and zod are peer dependencies and must be installed separately.
Quick Start
import { z } from 'zod';
import { defineContext, definePolicy, evaluatePolicy, allow } from '@bantai-dev/core';
import { createMemoryStorage } from '@bantai-dev/with-storage';
import {
withRateLimit,
defineRateLimitRule,
rateLimit,
} from '@bantai-dev/with-rate-limit';
// 1. Define your base context
const apiContext = defineContext(
z.object({
userId: z.string(),
endpoint: z.string(),
})
);
// 2. Extend context with rate limiting
// generateKey will automatically create keys from your input
const rateLimitedContext = withRateLimit(apiContext, {
storage: createMemoryStorage(rateLimit.storageSchema),
generateKey: (input) => `api:${input.userId}:${input.endpoint}`,
defaultValues: {
rateLimit: {
type: 'fixed-window',
limit: 100,
period: '1h',
},
},
});
// 3. Define a rate limiting rule using defineRateLimitRule
// This automatically handles rate limit checking and incrementing
const rateLimitRule = defineRateLimitRule(
rateLimitedContext,
'check-rate-limit',
async (input) => {
// Your business logic here
// Rate limit is already checked and will be incremented on allow
// input.currentLimit contains the rate limit check result
console.log(`Remaining: ${input.currentLimit.remaining}`);
return allow({ reason: 'Request allowed' });
},
{
config: {
limit: 100,
period: '1h',
type: 'fixed-window',
},
}
);
// 4. Define policy
const apiPolicy = definePolicy(
rateLimitedContext,
'api-rate-limit-policy',
[rateLimitRule],
{
defaultStrategy: 'preemptive',
}
);
// 5. Evaluate policy
// The generateKey function will create the key automatically
const result = await evaluatePolicy(apiPolicy, {
userId: 'user123',
endpoint: '/api/search',
});Rate Limiting Strategies
Fixed Window
Fixed window rate limiting divides time into discrete windows. All requests within a window count toward the limit, and the counter resets at the start of each new window.
Use cases: Simple rate limiting, API quotas, basic throttling
{
type: 'fixed-window',
key: 'user:123',
limit: 100,
period: '1h', // Supports ms format: '1h', '30m', '5s', etc.
}Sliding Window
Sliding window rate limiting tracks individual request timestamps. Only requests within the current window count toward the limit, providing smoother rate limiting.
Use cases: More accurate rate limiting, preventing burst traffic
{
type: 'sliding-window',
key: 'user:123',
limit: 100,
period: '1h',
}Token Bucket
Token bucket rate limiting uses a bucket that refills at a constant rate. Requests consume tokens, and requests are allowed when tokens are available.
Use cases: Burst handling, smooth rate limiting with refill
{
type: 'token-bucket',
key: 'user:123',
limit: 10000,
period: '1d', // Refills to full capacity (10k tokens) over 1 day
cost: 1, // Optional: tokens consumed per request (default: 1)
}Note: The period is a time period (e.g. "1d", "1h", "30m") representing the time to refill from empty to full capacity. The refill rate is automatically calculated as limit / period. For example, limit: 10_000 and period: '1d' means the bucket can hold 10,000 tokens and refills at a rate of 10,000 tokens per day.
Cost: The cost parameter (optional, default: 1) specifies how many tokens/requests each operation consumes. This allows you to implement variable-cost rate limiting where different operations consume different amounts. For example, a simple API call might cost 1, while a complex operation might cost 5. This parameter works for all rate limiting strategies.
API Reference
withRateLimit(context, options?)
Extends a Bantai context with rate limiting capabilities. Adds rateLimit schema fields and tools to the context.
Parameters:
context: A Bantai context definitionoptions:storage?: A storage adapter implementingRateLimitStorageinterfacedefaultValues?: Default values for rate limit configurationgenerateKey?: Optional function to generate rate limit keys dynamically from context input. If provided, this function will be used whenrateLimit.keyis not specified in the input.
Returns: Extended context with rate limiting capabilities
Example with generateKey:
import { createMemoryStorage } from '@bantai-dev/with-storage';
import { withRateLimit, rateLimit } from '@bantai-dev/with-rate-limit';
const rateLimitedContext = withRateLimit(apiContext, {
storage: createMemoryStorage(rateLimit.storageSchema),
generateKey: (input) => `api:${input.userId}:${input.endpoint}`,
defaultValues: {
rateLimit: {
type: 'fixed-window',
limit: 100,
period: '1h',
},
},
});When using defineRateLimitRule, if rateLimit.key is not provided in the input, the generateKey function will be used automatically.
defineRateLimitRule(context, name, evaluate, options)
A helper function that automatically handles rate limit checking and incrementing. This simplifies rule creation by handling the rate limit logic for you.
Parameters:
context: A context extended with rate limiting capabilities viawithRateLimitname: Unique name for the ruleevaluate: Your rule evaluation function. The function receives:input: The context input with an additionalcurrentLimitproperty containing the rate limit check resultcontext: The evaluation context with tools
options: Configuration objectconfig: Rate limit configuration (required). This will be merged with anyrateLimitconfig from the input.onAllow?: Optional hook called after rate limit is incremented (if your rule allows)onDeny?: Optional hook called if your rule denies
Returns: RuleDefinition<TContext, TName>
How it works:
- Checks the rate limit before evaluating your rule
- If rate limit is exceeded, returns
denyimmediately - If rate limit passes, evaluates your rule with
currentLimitavailable in the input - On allow, automatically increments the rate limit counter
- Calls your optional hooks
Key resolution order:
- If
rateLimit.keyis provided in the input, use it - Otherwise, if
generateKeyfunction is available, use it - Otherwise, fall back to
'unknown-key'
Example:
const rateLimitRule = defineRateLimitRule(
rateLimitedContext,
'api-rule',
async (input) => {
// input.currentLimit contains the rate limit check result
console.log(`Remaining: ${input.currentLimit.remaining}`);
// Your business logic here
return allow({ reason: 'Request processed' });
},
{
config: {
limit: 100,
period: '1h',
type: 'fixed-window',
},
onAllow: async (result, input) => {
// Optional: Additional logic after rate limit increment
console.log(`Request allowed for ${input.userId}`);
},
}
);rateLimit.checkRateLimit(storage, config, clock?)
Checks if a rate limit would be exceeded without incrementing the counter. Use this in your rule's evaluate function when not using defineRateLimitRule.
Parameters:
storage: Storage adapter implementingRateLimitStorageconfig: Rate limit configuration objectclock?: Optional clock function for testing (defaults toDate.now)
Returns: Promise<RateLimitCheckResult>
RateLimitCheckResult:
{
allowed: boolean;
remaining: number;
resetAt: number; // Unix timestamp in milliseconds
reason?: string;
}rateLimit.incrementRateLimit(storage, config, clock?)
Increments the rate limit counter. Use this in your rule's onAllow hook when not using defineRateLimitRule.
Parameters:
storage: Storage adapter implementingRateLimitStorageconfig: Rate limit configuration objectclock?: Optional clock function for testing (defaults toDate.now)
Returns: Promise<void>
Storage Integration
The rate limiting extension requires a storage adapter. You can use:
- Memory storage (development/testing):
createMemoryStoragefrom this package - Redis storage (production):
createRedisStoragefrom@bantai-dev/storage-redis - Custom storage: Implement the
StorageAdapterinterface from@bantai-dev/with-storage
Using Redis Storage
import { createRedisStorage } from '@bantai-dev/storage-redis';
import { rateLimit } from '@bantai-dev/with-rate-limit';
const redisStorage = createRedisStorage(
{ url: process.env.REDIS_URL },
rateLimit.storageSchema
);
const rateLimitedContext = withRateLimit(apiContext, {
storage: redisStorage,
});Examples
Per-User Rate Limiting
import { defineRule, allow, deny } from '@bantai-dev/core';
const userRateLimitRule = defineRule(
rateLimitedContext,
'user-rate-limit',
async (input, { tools }) => {
const result = await tools.rateLimit.checkRateLimit(
tools.storage,
{
key: `user:${input.userId}`,
type: 'fixed-window',
limit: 1000,
period: '24h',
}
);
return result.allowed ? allow({ reason: 'Rate limit OK' }) : deny({ reason: result.reason });
},
{
onAllow: async (result, input, { tools }) => {
await tools.rateLimit.incrementRateLimit(
tools.storage,
{
key: `user:${input.userId}`,
type: 'fixed-window',
limit: 1000,
period: '24h',
}
);
},
}
);Endpoint-Specific Rate Limits
import { defineRule, allow, deny } from '@bantai-dev/core';
const endpointLimits = {
'/api/auth/login': { limit: 5, period: '15m' },
'/api/payment': { limit: 10, period: '1m' },
'/api/search': { limit: 100, period: '1m' },
};
const endpointRateLimitRule = defineRule(
rateLimitedContext,
'endpoint-rate-limit',
async (input, { tools }) => {
const config = endpointLimits[input.endpoint] || { limit: 50, period: '1h' };
const result = await tools.rateLimit.checkRateLimit(
tools.storage,
{
key: `endpoint:${input.endpoint}:${input.userId}`,
type: 'sliding-window',
limit: config.limit,
period: config.period,
}
);
return result.allowed ? allow({ reason: 'Rate limit OK' }) : deny({ reason: result.reason });
},
{
onAllow: async (result, input, { tools }) => {
const config = endpointLimits[input.endpoint] || { limit: 50, period: '1h' };
await tools.rateLimit.incrementRateLimit(
tools.storage,
{
key: `endpoint:${input.endpoint}:${input.userId}`,
type: 'sliding-window',
limit: config.limit,
period: config.period,
}
);
},
}
);Tier-Based Rate Limiting
import { defineRule, allow, deny } from '@bantai-dev/core';
const tierLimits = {
free: { limit: 100, period: '1h' },
premium: { limit: 1000, period: '1h' },
enterprise: { limit: 10000, period: '1h' },
};
const tierRateLimitRule = defineRule(
rateLimitedContext,
'tier-rate-limit',
async (input, { tools }) => {
const config = tierLimits[input.userTier];
const result = await tools.rateLimit.checkRateLimit(
tools.storage,
{
key: `tier:${input.userTier}:${input.userId}`,
type: 'token-bucket',
limit: config.limit,
period: '1h', // Refills to full capacity over 1 hour
}
);
return result.allowed ? allow({ reason: 'Rate limit OK' }) : deny({ reason: result.reason });
},
{
onAllow: async (result, input, { tools }) => {
const config = tierLimits[input.userTier];
await tools.rateLimit.incrementRateLimit(
tools.storage,
{
key: `tier:${input.userTier}:${input.userId}`,
type: 'token-bucket',
limit: config.limit,
period: '1h', // Refills to full capacity over 1 hour
}
);
},
}
);Type Safety
The package provides full TypeScript type safety:
- Context extension: Type-safe context merging with rate limit fields
- Config validation: Zod schemas validate rate limit configurations
- Storage types: Type-safe storage adapter interface
- Result types: Typed rate limit check results
Requirements
- Node.js >= 20.9.0
- TypeScript >= 5.0
- Zod >= 4.3.5
- @bantai-dev/core
- @bantai-dev/with-storage
Links
- Website: https://bantai.vercel.app/
- GitHub Repository: https://github.com/bosquejun/bantai
- npm Package: https://www.npmjs.com/package/@bantai-dev/with-rate-limit
License
MIT
