12k
All articles

Node.jsにおけるミドルウェアの仕組み

Express ミドルウェアがリクエストライフサイクルで順番に実行される仕組み、next によるチェーン制御、Express 5 での非同期エラーの扱い方を解説する。

OpenReplay Team
OpenReplay Team
Node.jsにおけるミドルウェアの仕組み

Expressのコードベース全体に散りばめられたapp.use()を見たことがあるでしょう。ミドルウェアがリクエストとレスポンスの間に位置することは知っています。しかし、そのチェーンで何かが壊れたとき—リクエストがハングしたり、エラーが消えたり、ハンドラが間違った順序で実行されたりすると—メンタルモデルが崩壊します。

この記事では、Node.jsのミドルウェアが実際にどのように動作するかを説明します:それが何であるか、チェーンを通じて制御がどのように流れるか、そしてExpress 5で何が変わったかについてです。

重要なポイント

  • ミドルウェアはフレームワークパターンであり、Node.jsの機能ではありません—Expressはコアのhttpモジュールの上にそれを実装しています
  • 実行順序が重要です:ミドルウェアは登録順に実行され、エラーハンドラは最後に配置する必要があります
  • next()関数がフローを制御します;レスポンスを送信せずにそれをスキップすると、リクエストがハングします
  • Express 5は非同期ミドルウェアとルートハンドラからのrejectされたPromiseをネイティブにキャッチし、手動ラッパーの必要性を排除します

ミドルウェアはフレームワークパターンであり、Node.jsの機能ではない

Node.js自体にはミドルウェアの概念はありません。コアのhttpモジュールは、リクエストとレスポンスオブジェクトを提供するだけです。ミドルウェアは、ExpressのようなフレームワークがNode.jsの上に実装するパターンです。

Expressのミドルウェアは特定の構造に従います:reqresnextを受け取る関数です。フレームワークはこれらの関数のスタックを維持し、受信した各リクエストに対して順次呼び出します。このNode.jsフレームワークにおけるリクエストライフサイクルが、ミドルウェアにその力を与えています。

他のフレームワークは、同様のパターンを異なる方法で実装しています。Koaは、ミドルウェアが後続のハンドラをラップする「オニオンモデル」を使用します。Fastifyは、特定のライフサイクルポイントでフックを使用します。概念は移行できますが、セマンティクスは移行できません。

リクエスト-レスポンスライフサイクル

リクエストがExpressアプリケーションに到達すると、パイプラインに入ります:

  1. Expressは、登録されたミドルウェアとルートに対してリクエストをマッチングします
  2. 各ミドルウェア関数は登録順に実行されます
  3. 制御はnext()を介して前方に渡されるか、レスポンスが送信されたときに停止します
  4. エラーハンドリングミドルウェアは、next(err)に渡されたエラーと、Express管理のミドルウェアおよびルートハンドラ内でスローまたはrejectされたエラーをキャッチします

next()関数は、チェーンを通じて制御を移動させるメカニズムです。それを呼び出すと、Expressは次にマッチするミドルウェアを呼び出します。レスポンスを送信せずにそれをスキップすると、リクエストは無期限にハングします。

ミドルウェアの順序が重要

Expressのミドルウェアは、登録した順序で実行されます。これは単なる詳細ではありません—Node.jsにおけるミドルウェアパターンの基礎です。

app.use(parseBody)
app.use(authenticate)
app.use(authorize)
app.get('/data', handler)

ここでは、parseBodyが最初に実行され、リクエストデータをauthenticateで利用可能にします。順序を入れ替えると、ボディがまだパースされていないため認証が壊れます。

この順序はエラーハンドリングにも影響します。エラーハンドリングミドルウェアは、保護するルートの後に配置する必要があります。

レスポンスのショートサーキット

ミドルウェアは、next()を呼び出さずにレスポンスを送信することで、リクエスト-レスポンスサイクルを早期に終了できます。これは、認証ミドルウェアが未承認のリクエストを拒否する方法です:

function requireAuth(req, res, next) {
  if (!req.user) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  next()
}

res.send()res.json()、または類似のメソッドが実行されると、そのリクエストに対して後続のミドルウェアは実行されません。このショートサーキットは意図的で有用です。

アプリケーションレベル、ルーターレベル、ルートレベルのミドルウェア

Expressのミドルウェアは異なるスコープでアタッチされます:

アプリケーションレベルのミドルウェアは、アプリケーションへのすべてのリクエストに対して実行されます。ロギングやボディパースなどの横断的関心事にはapp.use()を使用します。

ルーターレベルのミドルウェアは、特定のルーターにマッチするリクエストに対して実行されます。これにより、アプリケーション全体に影響を与えることなく、ルートグループにミドルウェアをスコープできます。

ルートレベルのミドルウェアは、特定のルートに対してのみ実行されます。検証のようなターゲットを絞った動作のために、ミドルウェア関数をルート定義に直接渡します。

この区別はスコープに関するものであり、機能に関するものではありません。3つすべてが同じ関数シグネチャを使用します。

エラーハンドリングとExpress 5のミドルウェア

Expressは、4つのパラメータシグネチャ:(err, req, res, next)によってエラーハンドリングミドルウェアを識別します。ミドルウェアがnext(err)を呼び出すか、エラーをスローすると、Expressは通常のミドルウェアをスキップし、エラーハンドラにジャンプします。

Express 5は、非同期エラーの動作方法を変更します。Express 4では、rejectされたPromiseと非同期例外には手動処理またはサードパーティのラッパーが必要でした。Express 5は、非同期関数からのrejectされたPromiseをネイティブにキャッチし、自動的にエラーハンドラにルーティングします。

// Express 5: ラッパーは不要
app.get('/data', async (req, res) => {
  const data = await fetchData() // rejectはエラーミドルウェアをトリガーします
  res.json(data)
})

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message })
})

エラーハンドリングミドルウェアは、ルートや他のミドルウェアの後、最後に登録する必要があります。

まとめ

ミドルウェアは、関数のチェーンを通じてリクエストを処理するためのフレームワークレベルのパターンです。実行順序、next()の役割、エラーハンドラを配置する場所を理解することで、ミドルウェアのデバッグに関する頭痛の種のほとんどが解消されます。

Express 5のネイティブ非同期エラーハンドリングは、一般的な問題点を取り除きますが、基本は変わりません:ミドルウェアを正しい順序で登録し、next()を呼び出すかレスポンスを送信し、エラーハンドラを最後に配置します。

よくある質問

ミドルウェアでnext()を呼び出すのを忘れるとどうなりますか?

next()を呼び出すのを忘れ、レスポンスも送信しない場合、リクエストは無期限にハングします。Expressは、チェーンを続けるためにnext()が呼び出されるか、クライアントにレスポンスが送信されるのを待ちます。ミドルウェアが必ずnext()を呼び出して制御を前方に渡すか、サイクルを終了するためにレスポンスを送信するようにしてください。

Express 4のミドルウェアでasync/awaitを使用できますか?

はい、ただしエラーを手動で処理する必要があります。Express 4はrejectされたPromiseを自動的にキャッチしないため、未処理のrejectはエラーハンドリングミドルウェアをトリガーしません。非同期コードをtry-catchブロックでラップするか、エラーをnext()に転送するラッパー関数を使用してください。Express 5は、rejectをネイティブにキャッチすることでこの要件を排除します。

エラーハンドリングミドルウェアが呼び出されないのはなぜですか?

エラーハンドリングミドルウェアには、正確に4つのパラメータが必要です:err、req、res、next。いずれかのパラメータを省略すると、Expressはそれを通常のミドルウェアとして扱い、エラーハンドリング中にスキップします。また、エラーハンドラがアプリケーション内のすべてのルートと他のミドルウェアの後に登録されていることを確認してください。

app.use()とrouter.use()の違いは何ですか?

両方とも同じ関数シグネチャでミドルウェアを登録しますが、スコープが異なります。app.use()はアプリケーションレベルでミドルウェアをアタッチし、すべてのリクエストに対して実行されます。router.use()は特定のルーターインスタンスにミドルウェアをアタッチし、そのルーターのマウントされたパスにマッチするリクエストに対してのみ実行されます。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.