elysia-rate-limit

repository·main·Indexed 42 days ago

https://github.com/rayriffy/elysia-rate-limit

A lightweight rate-limiting plugin for Elysia.js (v5.1.1) featuring dynamic limits, custom key generation, and configurable error responses. It supports custom storage backends via the Context interface and provides flexible configuration for request durations, maximum request counts, and scoping.

Tokens
4.8K
Snippets
18
Records
25
Agent score
49%

What's inside elysia-rate-limit

  1. Basic Usage of rateLimit()

    main

    To use the rate limiter, import rateLimit from elysia-rate-limit and use it as a plugin in your Elysia instance.

    import { Elysia } from 'elysia'
    import { rateLimit } from 'elysia-rate-limit'
    
    new Elysia().use(rateLimit()).listen(3000)
  2. Install elysia-rate-limit

    main

    Install the plugin using bun:

    bun add elysia-rate-limit

    Version Compatibility

    Ensure your plugin version matches your Elysia version:

    • Elysia >= 2.0.0: Use elysia-rate-limit v5.0.0+
    • Elysia >= 1.0.0: Use elysia-rate-limit v3.0.0 - 4.6.3
    • Elysia < 1.0.0: Use elysia-rate-limit v2.0.0 - 2.2.0
  3. Configure the `duration` option

    main

    The duration defines the timeframe (in milliseconds) for which requests are remembered. It is also used for the Retry-After header.

    • Type: number | ((key: string, request: ExtendedRequest) => number | Promise<number>)
    • Default: 60000 (1 minute)

    You can provide a static number or a dynamic function. Dynamic values are resolved when a new window opens for a specific key; changes mid-window will only apply to the next window.

    // Static duration
    new Elysia().use(
      rateLimit({
        duration: 60_000,
      })
    )
    
    // Dynamic duration based on request headers
    new Elysia().use(
      rateLimit({
        duration: (key, request) => {
          const isPremium = request.headers.get('X-User-Tier') === 'premium'
          return isPremium ? 10_000 : 60_000
        },
      })
    )
  4. Configure the `errorResponse` option

    main

    Defines what happens when a client hits the rate limit.

    • Type: string | Response | Error
    • Default: 'rate-limit reached'

    Behaviors:

    • string: Returns a plain text response with status code 429.
    • Response: Returns the provided Response object as is.
    • Error: Throws the provided Error object.
    // Using a Response object
    new Elysia().use(
      rateLimit({
        errorResponse: new Response("rate-limited", {
          status: 429,
          headers: new Headers({
            'Content-Type': 'text/plain',
            'Custom-Header': 'custom',
          }),
        }),
      })
    )
    
    // Using an Error object
    new Elysia().use(
      rateLimit({
        errorResponse: new RateLimitError(),
      })
    )
  5. Configure the `max` option

    main

    The max option defines the maximum number of requests allowed within a single duration timeframe.

    • Type: number | ((key: string, request: ExtendedRequest) => number | Promise<number>)
    • Default: 10

    Like duration, this can be a static number or a dynamic function that receives the key and request object.

    // Static max requests
    new Elysia().use(
      rateLimit({
        max: 100,
      })
    )
    
    // Dynamic max requests based on database lookup
    new Elysia().use(
      rateLimit({
        max: async (key, request) => {
          const userTier = await getUserTier(key)
          return userTier === 'premium' ? 1000 : 100
        },
      })
    )
  6. Configure the `context` (Cache) implementation

    main

    The context manages the storage of request counts. By default, it uses an LRU cache with a maximum of 5,000 entries.

    To increase the cache size or provide a custom implementation, pass an instance of Context to the context option.

    • Type: Context
    • Default: DefaultContext (5,000 entries)
    import { DefaultContext } from 'elysia-rate-limit'
    
    new Elysia().use(
      rateLimit({
        // Increase max cache size to 10,000
        context: new DefaultContext(10_000),
      })
    )
  7. Initialize the rateLimit plugin

    main

    To use the rate limiter in your Elysia application, call the plugin function with an optional userOptions object and pass the result to app.use().

    Note: The plugin function itself is synchronous and returns a registration function. Do not await the plugin() call; instead, use it directly within app.use().

    import { Elysia } from 'elysia'
    import { plugin } from 'elysia-rate-limit'
    
    const app = new Elysia()
      .use(plugin())
      .get('/', () => 'Hello Elysia')
      .listen(3000)
  8. Customize the `generator` function

    main

    The generator function allows you to define how clients are identified (the key). By default, the plugin uses the client's IP address via server.requestIP().

    If you are behind a proxy (like NGINX or Cloudflare), you must implement a custom generator to extract the real client IP (e.g., from the CF-Connecting-IP header).

    • Signature: (request: Request, server: Server | null, derived: T) => MaybePromise<string>
    • Third Argument: You can use derived values from other plugins (like elysia-ip) if you provide the correct types.
    // Example: Custom generator for Cloudflare proxies
    const cloudflareGenerator = (req, server) => {
      const isFromCloudflare = verifyCloudflareIP(server?.requestIP(req)?.address)
      
      if (isFromCloudflare) {
        return req.headers.get('CF-Connecting-IP') ?? server?.requestIP(req)?.address ?? ''
      }
      
      return server?.requestIP(req)?.address ?? ''
    }
    
    new Elysia().use(rateLimit({ generator: cloudflareGenerator }))
  9. Configure elysia-rate-limit options

    main

    The Options interface defines the configuration for the elysia-rate-limit plugin. You can control how long requests are remembered, the maximum allowed requests, how clients are identified via a generator, and how the plugin responds when limits are exceeded.

    Key Configuration Properties

    • duration: The time window (in milliseconds) for which requests are tracked. Defaults to 60000. Can be a static number or a function (key: string, request: ExtendedRequest) => number | Promise<number> for dynamic windows.
    • max: The maximum number of requests allowed within the duration. Defaults to 10. Can be a static number or a function (key: string, request: ExtendedRequest) => number | Promise<number> for dynamic limits.
    • errorResponse: The response returned when a rate limit is reached. Can be a string, a Response object, or an Error.
    • scoping: Determines the limit's reach. 'global' (default) affects every request across the server, while 'plugin' limits only within the current plugin instance.
    • generator: A function used to categorize clients (e.g., by IP address or API key) to apply rate limits.
    • skip: A function to bypass rate limiting for specific requests. Defaults to always returning false. Signature: (req: ExtendedRequest, key?: string) => boolean | Promise<boolean>.
    • countFailedRequest: If true, failed requests will still count towards the rate limit. Defaults to false.
    • headers: If true (default), the plugin will automatically manage RateLimit-* headers. Set to false to disable.
    • context: The context used for storing request counts.
    • injectServer: An optional function to inject the server instance into the generator. Use this only if necessary, as it may slightly reduce performance.
  10. Customize the error response

    main

    By default, the plugin returns a 429 Too Many Requests status when the limit is exceeded. You can customize this behavior using the errorResponse option.

    Supported types for errorResponse:

    • Error: The plugin will throw this error.
    • Response: The plugin will return a clone of this response. If a Response is used, the plugin will automatically attempt to write rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and Retry-After) to the cloned response.
    app.use(plugin({
      errorResponse: new Response('Too many requests, please slow down.', { status: 429 })
    }))
  11. Handle failed requests and refunds

    main

    The countFailedRequest option determines whether failed requests (e.g., 404s, validation errors, or parse errors) should count against the rate limit.

    • If countFailedRequest is false (default behavior for errors), the plugin will automatically decrement the count for that clientKey, effectively 'refunding' the request.
    • If countFailedRequest is true, failed requests will consume the rate limit quota just like successful ones.