rateLimit function

Middleware rateLimit({
  1. int max = 60,
  2. Duration window = const Duration(minutes: 1),
  3. String keyGenerator(
    1. Context c
    )?,
  4. bool skip(
    1. Context c
    )?,
  5. Handler? onLimitExceeded,
  6. bool standardHeaders = true,
  7. RateLimitStore? store,
})

Rate-limiting middleware — caps requests per key within a time window.

Zero-dependency and in-memory by default (MemoryRateLimitStore); pass a custom store for a distributed backend. Emits the IETF draft RateLimit-* headers and Retry-After on rejection (429), and short-circuits the pipeline without running the handler.

import 'package:darto/rate_limit.dart';

// 100 requests / minute per client IP
app.use(rateLimit(max: 100, window: Duration(minutes: 1)));

// Per-user limit with a custom rejection and a skip rule
app.mount('/api/*', rateLimit(
  max: 20,
  keyGenerator: (c) => c.user?['id'] ?? c.req.ip,
  skip: (c) => c.req.path == '/api/health',
  onLimitExceeded: (c) => c.status(429).json({'error': 'slow down'}),
));

Implementation

Middleware rateLimit({
  int max = 60,
  Duration window = const Duration(minutes: 1),
  String Function(Context c)? keyGenerator,
  bool Function(Context c)? skip,
  Handler? onLimitExceeded,
  bool standardHeaders = true,
  RateLimitStore? store,
}) {
  final RateLimitStore backing = store ?? MemoryRateLimitStore();
  final String Function(Context) keyOf =
      keyGenerator ?? (Context c) => c.req.ip;

  return (Context c, Next next) async {
    if (skip != null && skip(c)) {
      await next();
      return;
    }

    final hit = await backing.hit(keyOf(c), window);
    final remaining = (max - hit.count).clamp(0, max);
    final resetIn = hit.resetAt.difference(DateTime.now()).inSeconds;
    final reset = resetIn < 0 ? 0 : resetIn;

    if (standardHeaders) {
      c.header('RateLimit-Limit', '$max');
      c.header('RateLimit-Remaining', '$remaining');
      c.header('RateLimit-Reset', '$reset');
    }

    if (hit.count > max) {
      c.header('Retry-After', '$reset');
      if (onLimitExceeded != null) {
        await onLimitExceeded(c);
      } else {
        c.status(429).json({'error': 'Too Many Requests'});
      }
      return; // short-circuit — handler not run
    }

    await next();
  };
}