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