Custom Stores
Create your own store to integrate with any storage backend. All stores must implement the HitLimitStore interface.
Store Interface
interface HitLimitStore {
hit(key: string, windowMs: number, limit: number): Promise<StoreResult> | StoreResult
reset(key: string): Promise<void> | void
shutdown?(): Promise<void> | void
}
interface StoreResult {
count: number
resetAt: number
} Basic Example
A simple in-memory store implementation:
import type { HitLimitStore } from '@joint-ops/hitlimit'
export function myStore(): HitLimitStore {
const data = new Map<string, { count: number; resetAt: number }>()
return {
hit(key, windowMs) {
const now = Date.now()
const entry = data.get(key)
// Reset if window expired
if (!entry || now >= entry.resetAt) {
const record = { count: 1, resetAt: now + windowMs }
data.set(key, record)
return record
}
// Increment existing
entry.count++
return { count: entry.count, resetAt: entry.resetAt }
},
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 { HitLimitStore } from '@joint-ops/hitlimit'
interface DbStoreOptions {
client: any
tableName?: string
}
export function dbStore(options: DbStoreOptions): HitLimitStore {
const { client, tableName = 'rate_limits' } = options
return {
async hit(key, windowMs) {
const now = Date.now()
const resetAt = now + windowMs
// 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