Menu

Expressのミドルウェアとは?next()の仕組みと使い方

Expressのミドルウェアは、ルートハンドラーの前に動く (req, res, next) の関数です。リクエストを読んだり変えたり、レスポンスを返して終わらせたり、next() を呼んで次に渡したりできます。

このページのコードはエディタで実行できます。編集してすぐに結果を確認できます。

Expressのミドルウェアは (req, res, next) というシグネチャを持つ関数で、リクエストが届いてからレスポンスが送られるまでの間に実行されます。それぞれのミドルウェアは req と res を読んだり変えたりでき、レスポンスを送ってリクエストを終わらせることも、next() を呼んで次の関数にリクエストを渡すこともできます。app.use(fn) はすべてのリクエストに対するミドルウェアを追加し、Expressは登録した順にすべてを実行します。

どのリクエストでも、Consoleには3つのステップが順番に記録され、ルートはロガーが追加したプロパティ req.startedAt を読んでいます。GET /missing を送ってみてください。app.use() はすべてのパスにマッチするので2つのミドルウェアはやはりログを出し、その後、マッチするルートがないのでExpressは404を返します。

next()を忘れる

レスポンスも送らず next() も呼ばないミドルウェアは、リクエストを宙ぶらりんにします。クライアントはタイムアウトするまで待ち、ログには何も出ません。そのため、これはミドルウェアでよくあるバグです。上の logger から next(); を消して GET / をもう一度送ると、リクエストに返事が来なくなります。

ルールは単純です。ミドルウェアを通るどの道も、次のどれか1つで必ず終わらせます。

  • 続けるなら next()
  • エラーハンドラーへ飛ぶなら next(err)(Expressのエラー処理を参照)
  • res.json() や res.status(401).send() のようなレスポンス

レスポンスを送った後に next() を呼ぶのもバグです。次のハンドラーが2回目のレスポンスを送ろうとし、Expressは ERR_HTTP_HEADERS_SENT を投げます。関数がそこで止まるように return res.status(401).json(...) と書きましょう。

リクエストを止める認証チェック

リクエストを拒否するミドルウェアは、自分でレスポンスを送り、next() は呼びません。ルートに追加の引数として渡せば、そのルートだけを守れます。

最初のリクエストには 401 と {"error":"Missing or wrong API key"} が返ります。リクエストバーでヘッダー x-api-key: secret123 を追加して送り直すと、今度は {"users":42,"caller":"reporting-app"} が返ります。/public ではチェックは実行されません。

ミドルウェアが動く範囲

書き方対象
app.use(fn)すべてのリクエスト、すべてのメソッド
app.use('/api', fn)/api で始まるパス
app.get('/admin', fn, handler)GET /admin だけ。handler の前
router.use(fn)そのルーターのすべてのルート

順番も意味の一部です。ルートの後に登録したロガーは、そのルートが答えたリクエストを決して記録しません。レスポンスを送ったルートは next() を呼ばないからです。ログ、ボディのパース、CORSのようなアプリ全体のミドルウェアを最初に、次にルート、最後に404とエラーのハンドラーを登録しましょう。

組み込みミドルウェア

Expressには、app.use() で追加するミドルウェア関数がいくつか付属しています。ボディパーサーは req.body を埋めます。これがないと req.body は undefined です。

ミドルウェア動作
express.json()JSONのボディ(Content-Type: application/json)をパースする
express.urlencoded()HTMLフォームの送信(name=Ada&age=36)をパースする
express.text()プレーンテキストのボディを文字列として req.body に入れる
express.static('public')フォルダのファイルを配信する

ボディ {"name": "Ada"} で POST /echo を送ると、返事に "body":{"name":"Ada"} が含まれます。express.json() の行を消して送り直すと、返事から body がなくなります。req.body が undefined になり、JSON.stringify はundefinedの値を省くからです。cors、helmet、morgan のようなパッケージもミドルウェアで、npm install して同じように app.use() に渡します。ファイルの配信についてはexpress.staticのページで扱っています。

よくある間違い

ミドルウェアを渡さずに呼んでしまう。 app.use(logger()) は起動時に logger を1回実行し、その戻り値を登録します。app.use(logger) と書きましょう(express.json() に括弧が付くのは、ミドルウェア関数を返すからです)。

ボディパーサーをルートの後に追加する。 ルートが先に動くので、req.body は undefined に見えます。

app.use('/admin', fn) が1つのパスだけにマッチすると思う。 /admin/users など /admin の下のすべてのパスで、すべてのメソッドに対して実行されます。1つのルートだけを守るなら、ミドルウェアを app.get('/admin', fn, handler) に渡します。

よくある質問

ミドルウェアとルートハンドラーの違いは何ですか?

技術的には違いはありません。どちらも (req, res, next) の関数です。慣習として、ルートハンドラーはレスポンスを返してリクエストを終わらせ、ミドルウェアは共通の処理をして next() を呼ぶことが多いです。

ミドルウェアをasyncにできますか?

できます。Express 5では、asyncのミドルウェアが例外を投げたりrejectされたPromiseを返したりすると、そのエラーは自動でエラーハンドラーに送られます。必要なものをawaitしてから next() を呼びましょう。

next('route')は何をしますか?

今のルートの残りのハンドラーを飛ばして、次にマッチするルートへ進みます。使えるのは app.get()、app.post() などのメソッド関数で追加したハンドラーだけで、app.use() では使えません。

1つのルートだけミドルウェアをスキップするには?

そのルートをミドルウェアより前に登録して先に答えさせるか、ミドルウェアの中でパスを確認します(if (req.path === '/health') return next();)。アプリ全体ではなくルーターやルートごとにミドルウェアを適用すれば、そもそも不要になることも多いです。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める