ExpressにおけるRate Limitの実装ガイド

# Rate Limitの概要
Rate Limitは、APIへのリクエスト数を制御するためのシステムです。
これは、APIを過剰に使用したり、悪意のある攻撃を防ぐために使用されます。
Rate Limitによって、一定期間内に許可されるリクエストの数が限定されることで、サーバーの過負荷を防ぎ、サービス品質を維持することができます。

## Rate Limitの利点
### サービスの過負荷防止
APIへの過剰なリクエストがサーバーのリソースを圧迫し、サービスが遅くなったり、最悪の場合はダウンしたりすることを防ぎます。
Rate Limitによって、予期せぬトラフィックの急増による障害からAPIを守ります。

### 悪意のある利用の防止
DDoS攻撃など、悪意のあるユーザーによる過剰なリクエストを防ぎます。
これにより、APIが安全に運用されることを確保します。

### 公平なリソースの利用
全てのユーザーに対して公平にAPIリソースへのアクセスを提供します。
一部のユーザーによる過剰なリクエストが他のユーザーの利用機会を奪うことを防ぎます。

### コスト管理
クラウドサービスなどのリソースを使用するAPIでは、リクエスト数に応じてコストが発生します。
Rate Limitによってリクエスト数を制御することで、運用コストの急増を防ぐことができます。

# express-rate-limit
`express-rate-limit`は、Node.jsのExpressアプリケーションに簡単にRate Limitを追加するためのミドルウェアです。
このライブラリは、APIエンドポイントへの過剰なアクセスを防ぎ、アプリケーションの負荷を軽減するために設計されています。
特に、WebサービスやAPIがDDoS攻撃やスクレイピングなどの悪意あるトラフィックに晒されるリスクを低減させます。

## 主な特徴
### 簡単なセットアップ
`express-rate-limit`は、数行のコードを追加するだけでExpressアプリケーションにRate Limitを実装できます。
### カスタマイズ
リクエスト数の制限、時間窓、レスポンスのカスタマイズなど、多くのオプションを通じて挙動を細かく調整できます。
### メモリストア組み込み
デフォルトでメモリストアを使用してリクエスト数を追跡しますが、Redisなどの外部ストアを組み込むことも可能です。

## 使用方法
### 必要なパッケージのインストール
まず、`Express`と`express-rate-limit`をインストールします。
ターミナルで以下のコマンドを実行します。
```bash
$ npm init -y
$ npm install express express-rate-limit typescript @types/node @types/express
$ npx tsc --init
```

### Expressアプリケーションの基本的なセットアップ
以下は、TypeScriptを使用したコードの例です。
APIエンドポイントに対して、15分間に10リクエストまでの制限を設けます。
```typescript:index.ts
import express from 'express';
import rateLimit from 'express-rate-limit';

// Rate Limitの設定
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  limit: 10, // 15分間に10リクエストまで許可
  standardHeaders: true, // Rate Limitヘッダーに関する情報を返す
  legacyHeaders: false, // 無効化されたRate Limitヘッダーを削除する
});

const app = express();

// APIルートにRate Limitを適用
app.use("/api/", apiLimiter);

// テスト用のAPIエンドポイント
app.get("/api/test", (req, res) => {
  res.json({ message: "Rate limit test API" });
});

// サーバーを起動
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
```

### コンパイルと実行
`index.ts`をコンパイルし、`index.js`を生成します。
```bash
$ npx tsc
```
生成されたJavaScriptファイルを実行します。
```bash
$ node index.js
Server running on port 3000
```
これで、Rate Limitが設定されたExpress APIサーバーを起動することができました。

### Rate Limitの確認
`localhost:3000/api/test`にアクセスします。
最初の10回は通常通りの応答が得られます。
![confirm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/15a97bc5-53dd-7319-9360-41820b799bfa.png)
11回目以降は制限に達したことを示す応答が返されるようになります。
これにより、Rate Limitの動作を確認できます。
![confirm-rate-limit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458297/835d708e-e232-319f-0a5c-147afdd31d4b.png)

## オプション
`express-rate-limit`のオプションを説明します。

- **windowMs**
    - **説明**: リクエストを記憶しておく期間（ミリ秒単位）
    - **デフォルト値**: `60000` ms（1分）
- **limit**
    - **説明**: `windowMs`の期間中にクライアントが許可される最大リクエスト数
    - **デフォルト値**: `5`
- **message**
    - **説明**: クライアントがRate Limitに達した際、返されるレスポンスボディ
    - **デフォルト値**: `'Too many requests, please try again later.'`
- **statusCode**
    - **説明**: クライアントがRate Limitに達した際、返されるHTTPステータスコード
    - **デフォルト値**: `429`（HTTP 429 Too Many Requests）
- **standardHeaders**
    - **説明**: 標準化されたRate Limitヘッダー（`RateLimit-*`）を送信するかどうか
    - **デフォルト値**: `false`（後方互換性のため）
- **legacyHeaders**
    - **説明**: `X-RateLimit-*`ヘッダーをRate Limit情報と共に送信するかどうか
    - **デフォルト値**: `true`（後方互換性のため）
- **keyGenerator**
    - **説明**: クライアントを識別するカスタムキーを生成するメソッド
    - **デフォルト値**: クライアントのIPアドレス
- **handler**
    - **説明**: クライアントがRate Limitに達した際に実行されるExpressリクエストハンドラー
    - **デフォルト値**: 設定された`statusCode`と`message`
- **store**
    - **説明**: クライアントのヒット数を格納するために使用するストア
    - **デフォルト値**: `MemoryStore`
- **skipFailedRequests**
    - **説明**: `true`に設定すると、4XXまたは5XXのステータスコードを持つレスポンスに対するリクエストは、クライアントのクォータにカウントされません
    - **デフォルト値**: `false`
- **skipSuccessfulRequests**
    - **説明**: `true`に設定すると、ステータスコードが400未満の成功したリクエストは、クライアントのクォータにカウントされません
    - **デフォルト値**: `false`
- **skip**
    - **説明**: 特定のリクエストがRate Limitのクォータにカウントされるかどうかを決定する。このオプションが`true`を返すと、そのリクエストはカウントされません
    - **デフォルト値**: `false`

これらのオプションを適切に設定することで、`express-rate-limit`を使用してアプリケーションのRate Limit要件に合わせたカスタマイズが可能になります。

## keyGeneratorのカスタマイズ
`keyGenerator`オプションは、リクエストを行っているクライアントを一意に識別するために使用されます。
`Request`オブジェクトから一意のキー（識別子）を生成し、そのキーを用いて各クライアントのリクエスト数をカウントします。
これにより、指定された時間窓（`windowMs`で設定）内に許可された最大リクエスト数（`limit`で設定）を超えたクライアントに対してRate Limitを適用することができます。

### keyGeneratorの役割
`keyGenerator`は、`Request`オブジェクトを引数として受け取り、文字列を返す必要があります。
デフォルトでは、クライアントのIPアドレスがキーとして使用されますが、`keyGenerator`をカスタマイズすることで、例えばユーザーID、APIキー、またはその他の識別情報を基にしたキーを生成することが可能です。

### カスタマイズ例
カスタマイズした`keyGenerator`を使用して、認証されたユーザーIDに基づいてRate Limitを適用する例は以下の通りです。
```javascript
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  limit: 100, // 各ユーザーごとに15分間に100リクエストまで許可
  keyGenerator: (req) => {
    // 認証されたユーザーのIDを基にキーを生成
    return req.user.id;
  },
  // その他のオプション...
});
```

このように`keyGenerator`をカスタマイズすることで、`express-rate-limit`の挙動をアプリケーションの特定のニーズに合わせて調整することができます。

## handlerのカスタマイズ
`express-rate-limit`の`handler`をカスタマイズすることで、Rate Limitに達した際の挙動を変更することができます。
以下に、カスタマイズの例をいくつか示します。

### リダイレクト
クライアントを特定のURLにリダイレクトさせたい場合は、`res.redirect()`を使用します。
```javascript
handler: (req, res) => {
  res.redirect('/some/path');
},
```

### カスタムHTMLページを送信
特定のHTMLページを表示したい場合、`res.sendFile()`を使用して静的ファイルを送信できます。
```javascript
handler: (req, res) => {
  res.sendFile(path.join(__dirname, 'path/to/your/rate-limit-page.html'));
},
```

### レスポンスヘッダーを設定
追加のレスポンスヘッダーを設定して、Rate Limit情報を提供したい場合、`res.set()`を使用します。
```javascript
handler: (req, res) => {
  res.set({
    'Retry-After': 3600, // 秒単位でクライアントに再試行を伝える
    'X-RateLimit-Reset': '時間のタイムスタンプ', // Rate Limitがリセットされる予定時刻
  }).status(429).json({
    message: "Too many requests. Please try again later."
  });
},
```

### ロギングや通知の実装
Rate Limitに達したことをロギングするか、システム管理者に通知するなど、バックエンドで何らかのアクションを実行したい場合は、`handler`内でそれらの処理を実装します。
```javascript
handler: (req, res) => {
  console.log(`Rate limit exceeded for ${req.ip}`);
  // ここでメール送信や外部APIを呼び出して通知する等の処理を行う
  res.status(429).json({
    message: "Rate limit exceeded. Please try again later."
  });
},
```

### 異なるレスポンス形式の提供
クライアントによって期待されるレスポンス形式が異なる場合（例えば、APIエンドポイントではJSONを、ブラウザではHTMLページを提供する）、リクエストのヘッダーやパラメーターに基づいて適切なレスポンスを動的に生成します。
```javascript
handler: (req, res) => {
  if (req.accepts('html')) {
    res.sendFile(path.join(__dirname, 'path/to/limit-page.html'));
  } else {
    res.status(429).json({ message: "Too many requests. Please try again later." });
  }
},
```
これらのカスタマイズを通じて、`express-rate-limit`の挙動をアプリケーションのニーズに合わせて柔軟に調整することが可能です。

## storeのカスタマイズ
`store`オプションを使用すると、リクエストのカウントを格納する方法をカスタマイズできます。
デフォルトでは`MemoryStore`が使用されますが、永続的なストアや他のカスタムストアを使用することで、複数のサーバーやプロセス間でRate Limitの状態を共有するなど、より高度なシナリオに対応できます。

### MemoryStoreを使用する例
```javascript
import rateLimit, { MemoryStore } from "express-rate-limit";

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分
  limit: 1, // 15分間に1リクエストまで許可
  message: "Too many requests from this IP, please try again after 15 minutes",
  statusCode: 429,
  standardHeaders: true,
  legacyHeaders: false,
  store: new MemoryStore(), // MemoryStoreを明示的に使用
});
```

### Redisを使用する例
`express-rate-limit`自体はRedisを直接サポートしていませんが、サードパーティのライブラリを使ってRedisを使用することができます。
以下は、`rate-limit-redis`を使用した例です。
```javascript
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

// Redisクライアントを作成（設定は環境に合わせて調整してください）
const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
  // その他のRedis設定
});

const apiLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
  }),
  windowMs: 15 * 60 * 1000, // 15分
  max: 1, // 15分間に1リクエストまで許可
  message: "Too many requests from this IP, please try again after 15 minutes",
  statusCode: 429,
  standardHeaders: true,
  legacyHeaders: false,
});
```
これにより、アプリケーションのRate Limitの状態がRedisに格納され、複数のプロセスやサーバー間で共有されるようになります。
これは、スケーラブルなアプリケーションや、複数のインスタンスで負荷を分散している場合に特に有用です。

# まとめ
`express-rate-limit`は、ExpressアプリケーションにRate Limitを簡単に追加し、アプリケーションの安定性を保つための効果的なツールです。
また、オプションにより、多様なニーズに対応することができます。

間違っている箇所や疑問に思ったことがあれば、ぜひコメントをください！
最後まで読んでいただき、ありがとうございました。

詳細なコードは、GitHubのリポジトリで公開しています。
興味のある方は是非チェックしてみてください。

https://github.com/GorillaSwe/rate-limit-tutorial-express