ウェブエンジニア問題集
第12章

Express入門 — ルーティング・ミドルウェア・エラーハンドリング

10
この章の目次開く

Node.js標準の http モジュールだけでもWebサーバーは作れます。しかし「URLごとの処理の振り分け」「リクエストボディのパース」「共通処理の差し込み」をすべて自前で書くことになり、コードはすぐに if 文の迷路になります。

Expressは、この定型部分を引き受けてくれる、Node.jsで最も広く使われてきたWebフレームワークです。この章では、Expressの3つの柱——ルーティングミドルウェアエラーハンドリング——を学びます。

学習者学習者

今からWebフレームワークを学ぶなら、もっと新しいものじゃなくてExpressでいいんですか?

先生先生

Expressの「ルーティング + ミドルウェア」という設計は、FastifyやHonoなど後発フレームワークにもほぼそのまま受け継がれているんだ。つまりExpressを理解すれば、他のフレームワークは「同じ概念の別記法」として読める。学ぶ価値は今でも十分あるよ。

最小のExpressサーバー

まずは動くものを見てみましょう。npmでインストールして、数行書くだけです。

npm install express
bash
const express = require('express');
 
const app = express(); // アプリケーション本体を作る
 
app.get('/', (req, res) => {
  res.send('Hello Express!');
});
 
app.listen(3000, () => {
  console.log('http://localhost:3000 で起動しました');
});
js

node app.js で起動してブラウザで http://localhost:3000 を開くと、Hello Express! が表示されます。

ガッツポーズする人
まずは最小構成で「動いた」を作ってから、部品を1つずつ理解していく

ルーティング — URLとメソッドで処理を振り分ける

ルーティングとは「どのURL・どのHTTPメソッドに、どの処理を対応させるか」の定義です。

構文: app.METHOD(path, handler)(METHODは get / post / put / delete など)

引数渡せるもの説明
path(第1引数)文字列URLのパス。:名前 でパスパラメータを定義できる
handler(第2引数以降)関数 (req, res, next)リクエスト処理。複数渡すと順番に実行される

戻り値: app 自身(メソッドチェーンできる)

// GET /users — 一覧
app.get('/users', (req, res) => {
  res.json(users);
});
 
// GET /users/42 — :id の部分が req.params.id に入る
app.get('/users/:id', (req, res) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'USER_NOT_FOUND' });
  }
  res.json(user);
});
 
// POST /users — ボディの値で作成
app.post('/users', (req, res) => {
  const created = createUser(req.body);
  res.status(201).json(created);
});
js

リクエストから値を取り出す3つの場所

取り出し元プロパティ
パスパラメータreq.params/users/:id + GET /users/42{ id: '42' }
クエリ文字列req.queryGET /users?page=2{ page: '2' }
リクエストボディreq.bodyPOST のJSONボディ(express.json() が必要、後述)
req.paramsreq.query の値はすべて文字列です。数値として使うなら Number() での変換と検証を忘れないでください。

レスポンスを返す

res の代表的なメソッドです。

メソッド説明
res.json(obj)オブジェクトをJSONで返す(Content-Typeも自動設定)。APIの基本形
res.status(code)ステータスコードを設定する。res.status(404).json(...) のようにチェーンする
res.send(data)文字列・HTML・Bufferなどを返す
res.redirect(url)リダイレクトする

ミドルウェア — Expressの本体

Expressの理解の核心はミドルウェアです。ミドルウェアとは、リクエストがルートハンドラに届く前後に差し込まれる関数のことで、Expressアプリの実体は「ミドルウェアが並んだパイプライン」です。

構文: ミドルウェアは (req, res, next) の3引数の関数

引数説明
reqリクエストオブジェクト。プロパティを追加して後続へ情報を渡せる
resレスポンスオブジェクト。ここでレスポンスを返して処理を打ち切ることもできる
next次のミドルウェアへ進む関数。呼ばないとリクエストが止まったままになる

app.use() で登録すると、すべてのリクエストに適用されます。

// 自作ミドルウェアの例: 全リクエストをログに残す
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next(); // これを呼ぶと次の処理へ進む
});
 
// 組み込みミドルウェア: JSONボディをパースして req.body に入れる
app.use(express.json());
 
// このルートに届く時点で、ログ記録とボディのパースが済んでいる
app.post('/users', (req, res) => {
  res.status(201).json(createUser(req.body));
});
js
ミドルウェアは登録した順番に実行されます。「パースより前にボディを使う」「認証より前に保護対象のルートを置く」といった順番の誤りが、Expressのバグの定番です。

ミドルウェアの形をしているものは自作に限りません。express.json()(ボディのパース)や express.static()(静的ファイル配信)のような組み込みのほか、CORS対応やセキュリティヘッダー付与などもnpmのミドルウェアとして提供されており、app.use() で差し込むだけで機能が増えていきます。

学習者学習者

next() を呼ぶのを忘れるとどうなるんですか?エラーになる?

先生先生

エラーにすらならないのが厄介なところ。リクエストはそのミドルウェアで止まったまま、クライアントは応答が返るのを永遠に待ち続けることになる。「特定のAPIだけタイムアウトする」という症状を見たら、next()の呼び忘れを疑うといいよ。

エラーハンドリング — 4引数の特別なミドルウェア

Expressでは、引数を4つ持つミドルウェアだけが「エラーハンドラ」として扱われるという規約があります。ルートハンドラでthrowされたエラーや next(err) で渡されたエラーは、通常のミドルウェアを飛ばしてエラーハンドラに直行します。

// エラーハンドラ: 引数が (err, req, res, next) の4つであることが目印
// すべてのルート定義より「後」に登録する
app.use((err, req, res, next) => {
  console.error(err.stack); // 詳細はログへ
 
  // ユーザーには整理された情報だけを返す
  res.status(500).json({
    error: { code: 'INTERNAL_ERROR', message: 'サーバーエラーが発生しました' },
  });
});
js

エラーハンドラを1か所に置くことで、各ルートに try...catch を散らばらせず、「予期するエラーは4xx、予期しないエラーは500 + ログ」というエラーハンドリングの章で学んだ方針をアプリ全体で統一できます。

非同期ハンドラのエラーに注意

ルートハンドラを async 関数にした場合の失敗の扱いは、Expressのバージョンで異なります。

app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id); // これが失敗すると?
  res.json(user);
});
js
  • Express 5 — async関数の拒否(rejection)は自動的にエラーハンドラへ渡されます
  • Express 4 — 自動では渡されません。try...catch して next(err) を呼ぶか、ラッパー関数を挟む必要があります
// Express 4での定番パターン: catchしてnextに渡すラッパー
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};
 
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  res.json(user);
}));
js

404はどう扱うか

どのルートにも一致しなかったリクエストは、ルート定義の最後に置いた「全部に一致するミドルウェア」で受けます。

// すべてのルート定義の後、エラーハンドラの前に置く
app.use((req, res) => {
  res.status(404).json({ error: { code: 'NOT_FOUND', message: '存在しないURLです' } });
});
js

ミドルウェアは順番に評価されるので、「最後まで誰も応答しなかった = 404」という構造がそのまま表現できるわけです。

よくあるハマりどころ

req.bodyundefined

express.json() を登録していないか、ルート定義より後に登録しているのが原因です。ボディを使うすべてのルートより前に app.use(express.json()) を置きます。

「Cannot set headers after they are sent」エラー

1つのリクエストに2回レスポンスを返そうとしたときのエラーです。典型例は、エラー分岐で res.status(404).json(...) した後に return を忘れ、続く res.json(...) も実行されるパターンです。レスポンスを返す行は return とセットを習慣にしましょう。

ミドルウェアの順番違いで認証がすり抜ける

認証ミドルウェアより前に定義されたルートは、認証なしでアクセスできます。「共通で守るものは先に app.use、公開ルートだけ認証の前に定義」のように、順番を意識して構成します。

この章のまとめ

  • Expressは「ルーティング + ミドルウェア」の薄いフレームワーク。この設計は後発フレームワークにも共通する
  • ルーティングは app.METHOD(path, handler)。入力は req.params / req.query / req.body の3か所から取り出す(値は文字列)
  • Expressアプリの実体は、登録順に実行されるミドルウェアのパイプライン——「順番」が設計の中心になる
  • エラー処理は4引数のエラーハンドラに集約する。asyncハンドラの失敗の扱いはExpress 4と5で違う点に注意
  • 404は「最後まで一致しなかったら」のミドルウェアで表現する

次章では、Expressアプリの先にあるデータベース接続と、環境変数・秘密情報・CORSといった実務のセキュリティ注意点を学びます。

参考リンク

Node.jsクイズに挑戦するこの章で学んだNode.jsの知識を、4択クイズでアウトプットして定着させよう