Custom Stores
Create your own store to integrate with any storage backend. All stores must implement the Store interface.
Store Interface
interface Store {
/**
* Increment the counter for a key
* @param key - Unique identifier (e.g., IP address)
* @param window - Time window in milliseconds
* @returns Current count and reset timestamp
*/
increment(key: string, window: number): Promise<{
count: number
resetAt: number
}>
/**
* Reset the counter for a key
* @param key - Unique identifier to reset
*/
reset(key: string): Promise<void>
} Basic Example
A simple in-memory store implementation:
import type { Store } from '@joint-ops/hitlimit'
export function myStore(): Store {
const data = new Map<string, { count: number; resetAt: number }>()
return {
async increment(key, window) {
const now = Date.now()
const entry = data.get(key)
// Reset if window expired
if (!entry || now >= entry.resetAt) {
const record = { count: 1, resetAt: now + window }
data.set(key, record)
return record
}
// Increment existing
entry.count++
return { count: entry.count, resetAt: entry.resetAt }
},
async reset(key) {
data.delete(key)
}
}
} Using Your Store
import { hitlimit } from '@joint-ops/hitlimit'
import { myStore } from './my-store'
app.use(hitlimit({
limit: 100,
window: '1m',
store: myStore()
})) Advanced: Database Store
Example using a generic database client:
import type { Store } from '@joint-ops/hitlimit'
interface DbStoreOptions {
client: any
tableName?: string
}
export function dbStore(options: DbStoreOptions): Store {
const { client, tableName = 'rate_limits' } = options
return {
async increment(key, window) {
const now = Date.now()
const resetAt = now + window
// Upsert with atomic increment
const result = await client.query(`
INSERT INTO ${tableName} (key, count, reset_at)
VALUES ($1, 1, $2)
ON CONFLICT (key) DO UPDATE SET
count = CASE
WHEN ${tableName}.reset_at <= $3 THEN 1
ELSE ${tableName}.count + 1
END,
reset_at = CASE
WHEN ${tableName}.reset_at <= $3 THEN $2
ELSE ${tableName}.reset_at
END
RETURNING count, reset_at
`, [key, resetAt, now])
return {
count: result.rows[0].count,
resetAt: result.rows[0].reset_at
}
},
async reset(key) {
await client.query(
`DELETE FROM ${tableName} WHERE key = $1`,
[key]
)
}
}
} Best Practices
- Atomic operations: Ensure increment is atomic to prevent race conditions
- TTL/Cleanup: Implement automatic cleanup of expired entries
- Error handling: Handle connection failures gracefully
- Performance: Minimize latency as this runs on every request