NodeJS 速習チュートリアル

Node.js ミドルウェア

1. ミドルウェアの導入

ミドルウェアはNode.jsのWebアプリケーション、特にExpress.jsにおいて極めて重要な役割を果たします。
これは、アプリケーションのルート(Route)やエンドポイント(Endpoint)全体で、共通の機能を再利用・追加するための仕組みを提供します。

ミドルウェアの主な特徴:

  • リクエスト・レスポンスサイクルの途中で実行される
  • リクエスト(req)およびレスポンス(res)オブジェクトを変更できる
  • リクエスト・レスポンスサイクルを終了させることができる
  • スタック内の次のミドルウェアを呼び出すことができる
  • アプリケーション・レベル、ルーター・レベル、または特定のルート限定で設定できる

ミドルウェアは、生のHTTPリクエストと、最終的なルートハンドラーとの間の「架け橋」として機能します。
本質的に、ミドルウェアは以下の要素にアクセスできる関数です:

  1. リクエストオブジェクト (req)
  2. レスポンスオブジェクト (res)
  3. アプリケーションのリクエスト・レスポンスサイクルにおける次のミドルウェア関数 (next)

ミドルウェア関数はさまざまなタスクを実行できます:

  • 任意のコードの実行
  • リクエストおよびレスポンスオブジェクトの変更
  • リクエスト・レスポンスサイクルの終了
  • スタック内の次のミドルウェア関数の呼び出し

ミドルウェアを、レスポンスを受け取る前にリクエストが通過する一連の処理レイヤー、つまりHTTPリクエストの「組み立てライン(アセンブリライン)」のように考えると分かりやすいでしょう。

2. リクエスト・レスポンスサイクルにおける動作

ミドルウェア関数は定義された順序で実行され、リクエストが流れるパイプラインを作成します。
各ミドルウェア関数は、リクエストとレスポンスオブジェクトに対して操作を行い、制御を次のミドルウェアに渡すか、あるいはリクエスト・レスポンスサイクルを終了させるかを決定します。

ミドルウェアを通過するリクエストのライフサイクル:

  1. サーバーがリクエストを受信
  2. 各ミドルウェアを順番に通過
  3. ルートハンドラーがリクエストを処理
  4. レスポンスが(逆順で)ミドルウェアを通って戻る
  5. クライアントにレスポンスを送信

Express.jsにおけるミドルウェアの基本パターンは以下の構造に従います:

app.use((req, res, next) => {
  // ここにミドルウェアのコードを記述
  console.log('時刻:', Date.now());
  
  // 次のミドルウェア関数に制御を移すために next() を呼び出す
  next();
});

next() を呼び出すと、スタック内の次のミドルウェアが実行されます。
next() を呼び出さない場合、リクエスト・レスポンスサイクルはそこで終了し、それ以降のミドルウェアは実行されません。

2.1 シンプルなミドルウェア・チェーンの例

const express = require('express');
const app = express();

// 第1のミドルウェア
app.use((req, res, next) => {
  console.log('ミドルウェア 1: これは常に実行されます');
  next();
});

// 第2のミドルウェア
app.use((req, res, next) => {
  console.log('ミドルウェア 2: これも常に実行されます');
  next();
});

// ルートハンドラー
app.get('/', (req, res) => {
  res.send('ハロー・ワールド!');
});

app.listen(8080, () => {
  console.log('サーバーがポート8080で起動しました');
});

ルートパス('/')にリクエストが送られると、以下が発生します:

  1. ミドルウェア 1 がログを出力し、next() を呼び出す
  2. ミドルウェア 2 がログを出力し、next() を呼び出す
  3. ルートハンドラーが「ハロー・ワールド!」というレスポンスを返す

3. ミドルウェアの種類の包括的ガイド

ミドルウェアの異なる種類を理解することは、アプリケーションのロジックを効果的に整理するのに役立ちます。
ミドルウェアは、そのスコープ(適用範囲)、目的、およびアプリケーションへのマウント方法に基づいて分類できます。

適切な種類の選択: 使用するミドルウェアの種類は、すべてのリクエストで実行すべきか、特定のルートのみか、あるいはルーターインスタンスへのアクセスが必要かなど、具体的なニーズによって決まります。

3.1 アプリケーション・レベルのミドルウェア

アプリケーション・レベルのミドルウェアは、app.use() または app.METHOD() 関数を使用して Express アプリケーションインスタンスにバインドされます。

  • ユースケース: ログ出力、認証、リクエストのパースなど、すべてのリクエストで実行すべき操作。
  • ベストプラクティス: ルートを定義する前にアプリケーション・レベルのミドルウェアを定義し、正しい順序で実行されるようにします。
const express = require('express');
const app = express();

// アプリケーション・レベルのミドルウェア
app.use((req, res, next) => {
  console.log('時刻:', Date.now());
  next();
});

3.2 ルーター・レベルのミドルウェア

ルーター・レベルのミドルウェアは、アプリケーション・レベルと同様に動作しますが、express.Router() インスタンスにバインドされます。

  • ユースケース: ルート固有のミドルウェアのグループ化、APIのバージョニング、ルートの論理的な整理。
  • 利点: コードの整理、モジュール化されたルーティング、特定のルートグループへのミドルウェア適用。
const express = require('express');
const router = express.Router();

// ルーター・レベルのミドルウェア
router.use((req, res, next) => {
  console.log('ルーター固有のミドルウェア');
  next();
});

router.get('/user/:id', (req, res) => {
  res.send('ユーザープロファイル');
});

// アプリにルーターを追加
app.use('/api', router);

3.3 エラーハンドリング・ミドルウェア

エラーハンドリング・ミドルウェアは 4 つの引数 (err, req, res, next) で定義され、リクエスト処理中に発生したエラーを処理するために使用されます。

  • ポイント: 必ず 4 つのパラメータを持つ必要がある。
  • 定義位置: 他の app.use() やルート呼び出しの後に定義する必要がある。
  • 利点: エラーハンドリングロジックを集中管理できる。
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('何かが壊れました!');
});

3.4 組み込みミドルウェア

Express には、一般的な Web アプリケーションタスクを処理するいくつかの組み込みミドルウェアが含まれています。

  • express.json(): JSON リクエストボディをパースする
  • express.urlencoded(): URL エンコードされたリクエストボディをパースする
  • express.static(): 静的ファイルを配信する
  • express.Router(): モジュール化されたルートハンドラーを作成する

ベストプラクティス: 組み込みミドルウェアは Express チームによって十分にテスト・メンテナンスされているため、可能な限りこれらを使用してください。

3.5 サードパーティ・ミドルウェア

Node.js エコシステムには、Express の機能を拡張する多数のサードパーティ製ミドルウェアパッケージがあります。

  • Helmet: さまざまな HTTP ヘッダーを設定してアプリを保護
  • Morgan: HTTP リクエストのロガー
  • CORS: 各種オプションで CORS を有効化
  • Compression: HTTP レスポンスを圧縮
  • Cookie-parser: Cookie ヘッダーをパースして req.cookies に格納

4. カスタムミドルウェアの作成と使用

カスタムミドルウェアを作成することで、アプリケーション固有の機能を再利用可能な形で実装できます。
適切に設計されたミドルウェアは、単一責任の原則に従い、テスト可能で、特定の役割に集中している必要があります。

4.1 シンプルなロガー・ミドルウェアの例

// シンプルなログ出力ミドルウェアを作成
function requestLogger(req, res, next) {
  const timestamp = new Date().toISOString();
  console.log(`${timestamp} - ${req.method} ${req.url}`);
  next(); // next() の呼び出しを忘れずに
}

// ミドルウェアを使用
app.use(requestLogger);

4.2 認証ミドルウェアの例

// 認証ミドルウェア
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).send('認証が必要です');
  }
  
  const token = authHeader.split(' ')[1];
  
  // トークンの検証 (簡略化された例)
  if (token === 'secret-token') {
    // 認証成功
    req.user = { id: 123, username: 'john' };
    next();
  } else {
    res.status(403).send('無効なトークンです');
  }
}

// 特定のルートに適用
app.get('/api/protected', authenticate, (req, res) => {
  res.json({ message: '保護されたデータ', user: req.user });
});

5. 非同期エラーの処理

非同期ミドルウェアでは、Promise の拒否(Rejection)をキャッチして next() に渡すようにしてください。

// 適切なエラー処理を備えた非同期ミドルウェア
app.get('/async-data', async (req, res, next) => {
  try {
    const data = await fetchDataFromDatabase();
    res.json(data);
  } catch (error) {
    next(error); // エラーをエラーハンドラーに渡す
  }
});

// 代替案:ラッパー関数(Async Handler)を使用
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

app.get('/better-async', asyncHandler(async (req, res) => {
  const data = await fetchDataFromDatabase();
  res.json(data);
}));

※Express 5(現在はベータ版)では、Promise の拒否を自動的にキャッチしてエラーハンドラーに渡すようになります。

6. ミドルウェアの実行順序

ミドルウェアを定義する順序は非常に重要です。Express はアプリケーションに追加された順序でミドルウェアを実行します。

推奨される順序:

  1. アプリケーション全体のミドルウェア: 全リクエストに適用されるもの(ロギング、セキュリティ、ボディパース)を最初に配置。
  2. ルート固有のミドルウェア: 特定のルートグループに適用するもの。
  3. ルート(Routes): メインのビジネスロジック。
  4. 404 ハンドラー: 一致するルートがなかった場合の処理。
  5. エラーハンドラー: 常に最後に配置。

7. ベストプラクティス

Node.js でミドルウェアを扱う際のベストプラクティスを以下にまとめます。

  1. 機能を絞る: 各ミドルウェアは単一責任の原則に従い、一つの役割に集中させる。
  2. Next() を適切に使う: レスポンスを終了させない限り、必ず next() を呼び出す。レスポンス送信後に next() を呼び出してはいけない。
  3. 非同期コードの適切な処理: 非同期ミドルウェア内のエラーは必ずキャッチして next(err) に渡す。
  4. ミドルウェアを多用しすぎない: 多すぎるミドルウェアはパフォーマンスに影響する可能性があるため、賢明に使用する。
  5. ドメインごとに整理する: 関連するミドルウェアは機能に基づいて別ファイルにグループ化する。

プロのヒント: 特定の設定を持つミドルウェアを生成する「ミドルウェア・ファクトリ」を作成すると、再利用性がさらに高まります。

// ミドルウェア・ファクトリ
function requireRole(role) {
  return (req, res, next) => {
    if (req.user && req.user.role === role) {
      next();
    } else {
      res.status(403).send('アクセスが拒否されました');
    }
  };
}

// 使用例
app.get('/admin', authenticate, requireRole('admin'), (req, res) => {
  res.send('管理者ダッシュボード');
});