メインコンテンツまでスキップ

レート制限

Zeltは、KVストアをバックエンドとした分散レート制限として @zeltjs/rate-limit パッケージを提供します。

基本的な使い方

routeにレート制限を適用するには @RateLimit デコレータを使います:

import { Controller, Get, Post } from '@zeltjs/core';
import { RateLimit } from '@zeltjs/rate-limit';

@Controller('/api')
export class ApiController {
  @RateLimit({ limit: 100, windowSec: 60, key: 'ip' })
  @Get('/data')
  getData() {
    return { items: [] };
  }
}

動的なキー

レート制限のキーは、リクエストがどのようにグルーピングされるかを決定します。静的な文字列または関数を使ってください:

@Controller('/api')
class ApiController {
  // IPアドレスごと
  @RateLimit({ limit: 100, windowSec: 60, key: 'ip' })
  @Get('/public')
  publicData() { return { data: [] }; }

  // ユーザーIDごと
  @RateLimit({
    limit: 1000,
    windowSec: 60,
    key: () => `user:${currentUser()?.id ?? 'anonymous'}`,
  })
  @Get('/user-data')
  userData() { return { data: [] }; }

  // APIキーごと
  @RateLimit({
    limit: 500,
    windowSec: 60,
    key: () => `apikey:${request().header('X-API-Key')}`,
  })
  @Get('/api-data')
  apiData() { return { data: [] }; }
}

プログラムからの利用

カスタムのレート制限ロジックには RateLimitService を使います:

@Controller('/auth')
export class AuthController {
  constructor(private rateLimiter = inject(RateLimitService)) {}

  @Post('/login')
  async login(req = request(LoginSchema), res = response()) {
    const body = await req.body();
    const result = await this.rateLimiter.hit(`login:${body.email}`, {
      limit: 5,
      windowSec: 300,
    });

    if (!result.ok) {
      return res.json({ error: 'Service unavailable' }, 503);
    }
    if (!result.value.allowed) {
      return res.json({ error: 'Too many attempts' }, 429);
    }
    return { token: 'jwt-token' };
  }

  @Post('/reset')
  async resetLimit(email: string) {
    await this.rateLimiter.reset(`login:${email}`);
    return { success: true };
  }
}

カスタム設定

RateLimitConfig を継承して挙動をカスタマイズします。デフォルトのインメモリストアの代わりにRedisをlimiterのバックエンドにするには、RedisKVAdaptorsuper() に渡します:

@Config
class CustomRateLimitConfig extends RateLimitConfig {
  constructor(kv = inject(RedisKVAdaptor)) {
    super(kv);
  }

  override readonly kvStoreNamespace = 'ratelimit:';
  override readonly defaultLimit = 200;
  override readonly defaultWindowSec = 120;
  override readonly failureMode = 'closed' as const;
}

Redisを使うには、adaptorが接続を解決できるよう RedisConfig(@zeltjs/redis から)を登録する必要があります。

レスポンスヘッダーとエラー

レート制限の情報は、レスポンスヘッダー X-RateLimit-LimitX-RateLimit-Remaining に含まれます。

StatusCodeいつ発生するか
429RATE_LIMIT_EXCEEDEDレート制限を超過した
503SERVICE_UNAVAILABLEclosed モードでKVストアが失敗した

失敗モード

failureMode オプションは、KVストアが利用できないときの挙動を制御します:

モード挙動
'open'(デフォルト)KVストアが失敗した場合、リクエストを通過させる
'closed'KVストアが失敗した場合、503でリクエストを拒否する

可用性を優先する重要度の低いレート制限には 'open' を使ってください。セキュリティが重要な厳格なレート制限には 'closed' を使ってください。

RateLimitResult型

hit() メソッドは Promise<RateLimiterHitResult> を返します:

type HitResult =
  | { ok: true; value: RateLimitResult }
  | { ok: false; error: RateLimitError };

type Result = {
  allowed: boolean;      // リクエストが許可されるかどうか
  remaining: number;     // 現在のwindowで残っているリクエスト数
  limit: number;         // 許可される最大リクエスト数
  retryAfterSec: number; // windowがリセットされるまでの秒数(許可されている場合は0)
};