rateLimit function
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();
};
}