On this page

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