Node.js

Express.jsとは?Express 5対応のバックエンド入門と4系からの移行点

Express.jsはNode.jsでバックエンドを書くときの事実上の標準フレームワークですが、いま調べ物をすると厄介な状態にあります。日本語の解説記事の多くがExpress 4を前提に書かれている一方、npmで npm install express と打つと入るのはExpress 5だからです。req.body{} ではなく undefined になる、app.get('/*') がエラーになる——記事どおりに書いたのに動かない原因はたいていここにあります。この記事は、Express.jsが何を肩代わりしているのかをNode.js標準の node:http との差分で押さえたうえで、Express 5時点の実装と4系からの移行点を整理します。

まとめ

  • Express.jsとは:Node.jsの node:http だけでは手書きになるURL分岐(ルーティング)、共通処理の連鎖(ミドルウェア)、静的ファイル配信、レスポンス生成をまとめて引き受ける最小構成のWebフレームワーク。DBもテンプレートも同梱しない。
  • 現行はExpress 5。5.1.0が2025年3月31日にnpmの latest となり、2026年7月時点の最新は5.2.1。4系は latest-4 タグで4.22.2が配布されている。
  • Express 4はMAINTENANCE(2025年4月1日〜)。セキュリティ修正と重大バグ修正のみで、EOLは公式に「2026年10月1日より前にはしない」とされている。新規開発は5系一択。
  • 移行で最初に踏む地雷は3つ:ボディ未パース時の req.bodyundefined、ワイルドカードは /*splat のように命名必須、res.send(200) のような数値ステータスの廃止。
  • async関数のthrowが自動でエラーハンドラへ流れるのがExpress 5最大の実利。4系で必要だった express-async-errorstry/catch + next(err) の定型が不要になる。
  • Node.jsは18以上が必須。本番はActive LTSのNode.js 24を推奨(26は2026年7月時点でCurrent)。

Express.jsとは:node:httpとの差分で理解する

Express.jsは2010年にTJ Holowaychukが公開し、現在はOpenJS Foundationのプロジェクトとして開発されているNode.js用のWebフレームワークです。「何ができるか」を機能リストで並べるより、Node.jsに最初から入っている node:http で同じものを書いたときに何が残るかを見たほうが、役割は正確につかめます。

Expressが肩代わりしている処理

Node.js標準の node:http でJSONを返すAPIを2本用意すると、こうなります。

import http from 'node:http';

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (req.method === 'GET' && url.pathname === '/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ id: 1 }]));
    return;
  }
  if (req.method === 'POST' && url.pathname === '/users') {
    let body = '';
    req.on('data', (chunk) => { body += chunk; });
    req.on('end', () => {
      const data = JSON.parse(body);   // 壊れたJSONで例外、サイズ上限も自前
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
    });
    return;
  }
  res.writeHead(404).end();
});
server.listen(3000);

メソッドとパスの分岐、ボディのストリーム受信とJSONパース、Content-Typeとステータスコードの手当て、404のフォールバック——アプリ固有のロジックはほとんど書いていないのに、定型処理で埋まります。ルートが20本に増えれば、この if の連鎖は破綻します。

Expressは同じものをこう書きます。

import express from 'express';

const app = express();
app.use(express.json());

app.get('/users', (req, res) => {
  res.json([{ id: 1 }]);
});

app.post('/users', (req, res) => {
  res.status(201).json(req.body);
});

app.listen(3000);

Expressが引き受けているのは、(1)メソッド+パスによるルーティング、(2)リクエストを順に通す共通処理=ミドルウェア、(3)ボディのパースや静的ファイル配信といった定番処理、(4) res.json() のようなレスポンス生成のショートカット、の4点です。逆に、データベース接続・認証・バリデーション・ORMは一切同梱していません。RailsやDjangoのようなフルスタックを期待すると肩透かしを食いますが、必要なものだけをnpmから選んで足す前提の設計であり、この薄さがExpressの性格そのものです。

「Expressを使わない」判断が成り立つ場面

Node.js 18でグローバルな fetch が実験的機能として入り、21で安定化しました。URL なども含めWeb標準APIが本体で使えるため、素の node:http でも昔ほど苦しくありません。ルートが2〜3本のヘルスチェック用サーバーや、AWS Lambdaのようにルーティングをプラットフォーム側(API Gateway)が持つ構成では、Expressを挟む必然性は薄くなります。依存ゼロで動くこと自体が価値になる場面もあります。

判断基準ははっきりしていて、ルートが増えるか、共通処理(認証・ログ・CORS・エラー整形)を横断的にかけたいかです。どちらかがYesなら、自前のディスパッチャを育てるより最初からExpressに乗せたほうが安く済みます。どちらもNoなら、node:http のままでかまいません。

Express 5と4の違い:移行前に押さえる破壊的変更

ここが現在いちばん事故が起きる場所です。Web上の日本語記事の大半はExpress 4を前提にしていますが、新規インストールで入るのは5系です。

バージョンの現況とサポート期限

Express 5.0.0は2024年9月に公開され、5.1.0が2025年3月31日にnpmの latest タグを取得しました。2026年7月時点の最新版は5.2.1で、4系は latest-4 タグから4.22.2が取得できます。

公式のLTSポリシーでは、4系は5.1がlatestになった2025年4月1日にMAINTENANCEフェーズへ移行し、以後はセキュリティ修正と優先度の高いバグ修正だけを受けます。EOL(サポート終了)については「2026年10月1日より前にはしない(no sooner than)」と明示されており、確定日ではなく最短ラインです。4系のまま運用しているプロジェクトは、機能追加が来ない前提でスケジュールを引く必要があります。

Express 5の動作要件はNode.js 18以上です。本番環境ならActive LTSのNode.js 24を選ぶのが無難で、Node.js 26は2026年7月時点ではまだCurrent(LTS化前)です。各系統のサポート期限はNode.js 26の新機能・変更点とLTSスケジュール総まとめで確認できます。プロジェクトごとにNodeのバージョンを切り替えたい場合はNodebrewとは?Node.jsのバージョン管理・インストール・切り替えを入門解説が使えます。

実装に効く破壊的変更

公式の移行ガイドから、既存コードが実際に動かなくなる変更を抜き出すと次のとおりです。

項目 Express 4 Express 5
未パース時の req.body {} undefined
ワイルドカード /* /*splat(命名必須・値は配列)
任意パラメータ /:file.:ext? /:file{.:ext}
数値ステータス送信 res.send(200) 廃止 → res.sendStatus(200)
ボディ+ステータス res.json(obj, 201) 廃止 → res.status(201).json(obj)
リダイレクト res.redirect(url, 301) res.redirect(301, url)(引数順が逆)
req.query 書き換え可 getter(代入不可)
urlencodedextended 既定 true 既定 false
express.staticdotfiles 既定で配信 既定 ignore
削除メソッド app.del() app.delete()
削除メソッド req.param() req.params / req.query / req.body
削除メソッド res.sendfile() res.sendFile()

実務でいちばん多いのは req.body です。Express 4では express.json() を付け忘れても req.body は空オブジェクトで、req.body.name は静かに undefined を返していました。Express 5では req.body 自体が undefined なので、同じコードが TypeError: Cannot read properties of undefined で落ちます。壊れ方が派手になっただけで、原因は昔からある「パーサ未登録」です。

ルーティング側は、内部のパスマッチャがpath-to-regexp v8に上がった影響です。app.get('/*', handler) のような無名ワイルドカードは書けなくなり、app.get('/*splat', handler) と名前を付けます。捕捉した値は req.params.splat配列で入る点も4系と違います。正規表現文字を直接埋めていた '/[a|b]/:slug' のような書き方は、パスの配列 ['/a/:slug', '/b/:slug'] に置き換えます。

Express 5で消える定型:async関数のエラー処理

移行の手間に見合う最大の見返りがこれです。Express 4では、非同期ハンドラ内で throw された例外はフレームワークに捕捉されず、素通りしてプロセスを落とすか、リクエストが応答なしのままハングしました。だから全ハンドラに try/catch を書いて next(err) に渡すか、express-async-errors のようなパッチを当てるのが定石でした。

Express 5は、ハンドラが返した拒否済みPromiseをエラーハンドリングミドルウェアへ自動的に転送します

// Express 5:try/catch も next(err) も不要
app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id);   // ここで reject しても
  if (!user) throw new NotFoundError();            // ここで throw しても
  res.json(user);                                  // 下のエラーハンドラへ届く
});

// エラーハンドラは引数4つ(この形でだけエラー用と認識される)
app.use((err, req, res, next) => {
  const status = err.status ?? 500;
  res.status(status).json({ message: err.message });
});

エラーハンドラは引数が4つerr, req, res, next)であることでエラー用と判別されるため、next を使わないからと省略すると通常のミドルウェア扱いになり、エラーを受け取れません。また、登録順の都合ですべてのルート定義より後ろに置く必要があります。

4系から5系への移行手順と、起動時に検出できない変更点

段階的にやるなら順序は決まっています。まずNode.jsを18以上(できればActive LTS)に上げ、次に npm install express@5 でアップグレードし、express.json() / express.urlencoded({ extended: true }) を明示的に登録し直します(extended の既定が変わったため、ネストしたフォームデータを扱っているなら明示が必要です)。そのうえで res.send(200)res.json(obj, status)res.redirect(url, status)app.del(req.param('/*' をコード全体でgrepし、順に置き換えます。ルーティングの変更はサーバー起動時にパスの解析エラーとして落ちるため、静的な検出が効きます。逆に req.bodyundefined 化と res.redirect の引数順は起動時には検出されず、実行時に初めて壊れるので、APIテストを先に用意してから着手するのが安全です。Expressのルートは supertest でHTTPレベルのテストが書けます(テストランナー側の選定はVitest 4の新機能と移行ガイド|ブラウザモード正式化・VRT・最新バージョンを参照)。

Expressの使い方:インストールからAPI公開まで

セットアップとサーバー起動

ESM(import)で書くため "type": "module" を指定します。

mkdir my-api && cd my-api
npm init -y
npm pkg set type=module
npm install express        # 5.2.1 が入る(2026年7月時点)
npm install -D nodemon
// app.js
import express from 'express';

const app = express();
const PORT = process.env.PORT ?? 3000;

app.use(express.json());                                  // JSONボディを解釈
app.use(express.urlencoded({ extended: true }));          // フォームのネストを許可
app.use(express.static('public'));                        // public/ を静的配信

app.get('/health', (req, res) => res.json({ status: 'ok' }));

app.listen(PORT, () => console.log(`listening on ${PORT}`));

node app.js で起動し、http://localhost:3000/health{"status":"ok"} を返せば成功です。開発中は npx nodemon app.js にすると保存のたびに再起動します。express.static('public') を入れた場合、public/style.css/style.css で配信されます(ディレクトリ名はURLに出ません)。Express 5では dotfiles の既定が ignore に変わったため、.env のようなドットファイルは公開ディレクトリに置いても配信されません(4系は既定で配信していました)。

ルーティング:メソッド・パスパラメータ・クエリ

// パスパラメータ: /users/42 → req.params.id === '42'(文字列)
app.get('/users/:id', (req, res) => {
  res.json({ id: Number(req.params.id) });
});

// クエリ文字列: /search?q=node&page=2 → req.query.q, req.query.page
app.get('/search', (req, res) => {
  const { q, page = '1' } = req.query;   // 5系では req.query への代入は不可
  res.json({ q, page: Number(page) });
});

// Express 5のワイルドカード:命名必須・値は配列
app.get('/files/*splat', (req, res) => {
  res.json({ segments: req.params.splat });   // /files/a/b → ['a','b']
});   // 注: この書き方では /files 自体は未マッチ。含めるなら '/files{/*splat}'

req.params の値は常に文字列です。数値として扱うなら明示的に変換します。ルートが増えたら express.Router() でファイルを分け、app.use('/users', usersRouter) のようにプレフィックスを付けてマウントします。

POSTのJSON・フォームデータを受け取る(body-parserは不要)

古い記事は必ず npm install body-parser から始まりますが、Express 4.16以降、body-parserの機能はExpress本体に取り込まれています。追加インストールは不要で、express.json()express.urlencoded() をそのまま使います。

app.use(express.json({ limit: '1mb' }));                 // Content-Type: application/json
app.use(express.urlencoded({ extended: true }));         // HTMLフォーム(x-www-form-urlencoded)

app.post('/users', (req, res) => {
  if (!req.body?.name) {                                 // 5系では req.body 自体が undefined になりうる
    return res.status(400).json({ message: 'name is required' });
  }
  res.status(201).json({ id: 1, name: req.body.name });
});

req.bodyundefined のままなら、原因はほぼ2つです。express.json()app.post() より後ろに登録している(ミドルウェアは登録順に実行されるため間に合わない)か、クライアントが Content-Type: application/json を送っていないかです。パーサはContent-Typeを見て動くので、ヘッダが無ければ何もしません。なお、ファイルアップロード(multipart/form-data)はExpress本体では扱えず、multer などを別途入れます。

ミドルウェアの仕組み:next()と登録順

ミドルウェアは (req, res, next) を受け取る関数で、登録した順にリクエストを通していきます。next() を呼べば次へ進み、res.json() などで応答すればそこで打ち切られます。next() も応答もしなければ、リクエストは応答を返さないまま宙吊りになります——ミドルウェア絡みのハングは、ほぼこの呼び忘れです。

// 全リクエストの所要時間をログに出す
app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - start) / 1e6;
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${ms.toFixed(1)}ms`);
  });
  next();                       // 呼ばないとここで止まる
});

// 特定パスにだけ適用(認証など)
app.use('/admin', (req, res, next) => {
  if (!req.headers.authorization) return res.status(401).end();
  next();
});

この「登録順がすべて」という性質から、実務での配置は決まってきます。ログやCORSのような全体にかけるものを最上段、express.json() などのパーサをルート定義より前、認証をそれを必要とするルートの直前、そしてエラーハンドラを最下段に置きます。express.json() をルートの後ろに書いてボディが取れない、というのは順序を意識していないときの典型的な躓きです。

バックエンドフレームワークの選定:Fastify・Hono・NestJSとの使い分け

Express・Fastify・Hono・NestJSの設計思想と適用場面

フレームワーク 設計思想 向く場面
Express 最小構成・自由に組み立てる 一般的なREST API、既存資産・情報量を重視
Fastify JSON Schemaでの検証・シリアライズを内蔵 スキーマ駆動でスループットを詰めたいAPI
Hono Web標準のRequest/Responseベース Cloudflare Workers・Deno・Bunなど複数ランタイム
NestJS DI+デコレータでレイヤを規定(既定でExpress上に構築) 大規模・多人数で構造を強制したい開発

注意したいのは、NestJSがExpressの競合ではなく上位レイヤだという点です。NestJSは既定でExpressをHTTPアダプタとして内部に抱えており、「NestJSを選ぶ」ことは「Expressを捨てる」ことを意味しません。逆に、HonoはNode.js固有のAPIに依存せずWeb標準のRequest/Responseで組まれているため、エッジランタイムへ載せる前提ならExpressより素直に収まります。

Expressを選ぶべきでない場面

立場をはっきり書きます。次の3つに当てはまるなら、Expressは第一候補から外して構いません。

1つ目は、Cloudflare Workersなどのエッジ環境で動かす場合。 ExpressはNode.jsの http モジュールに強く依存しており、Web標準APIベースのランタイムでは互換レイヤ頼みになります。最初からHonoで書くほうが摩擦が少なくなります。

2つ目は、10人以上でモノリスを育てる場合。 Expressは構造を一切強制しないため、ディレクトリ構成もDIも各自の裁量になり、規模が増えるほど設計のばらつきが直接コストになります。レイヤ構造とDIを言語仕様レベルで強制するNestJSのほうが、長期的な保守は安定します。

3つ目は、リクエスト単価のスループットが要件に直結する場合。 Fastifyはレスポンスのシリアライズ自体をJSON Schemaから最適化する設計で、同じJSONを返す用途ではExpressより有利です。ただし、Webアプリのレイテンシは大半がDBと外部API待ちで決まります。「フレームワークが速い」ことを理由にExpressを避ける前に、実測でボトルネックがフレームワーク層にあることを確認してください。多くの場合そうはなりません。

本番運用の初期設定:CORS・エラー処理・TypeScript

CORS:別オリジンのフロントから叩けるようにする

ReactやVueの開発サーバー(例:http://localhost:5173)からExpress(http://localhost:3000)を呼ぶと、ブラウザがCORSポリシーでブロックします。ExpressにCORS機能は同梱されていないため、Express公式org(expressjs/cors)が管理するミドルウェアを入れます。

npm install cors
import cors from 'cors';

app.use(cors({
  origin: ['http://localhost:5173', 'https://app.example.com'],   // 許可オリジンを列挙
  credentials: true,                                              // Cookie を跨いで送る場合
}));

app.use(cors()) と引数なしで書くと Access-Control-Allow-Origin: * になり、あらゆるオリジンからの呼び出しを許します。動作確認では手軽ですが、本番でそのまま残さないでください。なお credentials: true とワイルドカードのオリジンは仕様上併用できず、その場合は必ずオリジンを列挙する必要があります。

エラーハンドラ・404ハンドラの登録順と最下段配置

// すべてのルート定義の後ろ:どのルートにも当たらなかった場合
app.use((req, res) => {
  res.status(404).json({ message: 'Not Found' });
});

// さらに後ろ:引数4つのエラーハンドラ
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status ?? 500).json({
    message: err.status ? err.message : 'Internal Server Error',   // 500の詳細は外に出さない
  });
});

500系のエラーメッセージやスタックトレースをそのままJSONで返すと、内部構造の露出につながります。ステータスを持たない想定外の例外は、固定文言に丸めてログにだけ詳細を残します。

TypeScript対応:@types/expressとRequest拡張

Express本体はJavaScriptで書かれており、型定義は @types/express を別途入れます(express.d.ts を自作する必要はありません)。

npm install -D typescript @types/express @types/node tsx
import express, { Request, Response } from 'express';

const app = express();

app.get('/users/:id', (req: Request, res: Response) => {
  res.json({ id: req.params.id });
});

認証ミドルウェアで req.user のような独自プロパティを生やすと、TypeScriptは「Requestuser は存在しない」と怒ります。as any で潰さず、宣言のマージで Request 自体を拡張するのが定石です。

// types/express.d.ts
declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
    }
  }
}
export {};

このファイルを tsconfig.jsoninclude に含めれば、以後 req.user?.role が型付きで参照できます。

よくある質問(FAQ)

Express.jsとNode.jsの違いは?

Node.jsはJavaScriptの実行環境(ランタイム)そのもので、Express.jsはそのNode.js上で動くWebフレームワークです。Node.jsだけでもサーバーは書けますが(標準の node:http)、ルーティングやボディのパースを自前で組む必要があります。Expressはその定型部分を引き受けるライブラリで、Node.jsを置き換えるものではありません。

Express 4と5のどちらを使うべき?

新規開発は5系です。npm install express で入るのが5系(2026年7月時点で5.2.1)であり、4系は2025年4月からMAINTENANCEフェーズでセキュリティ修正と重大バグ修正のみを受けます。EOLは公式に「2026年10月1日より前にはしない」とされているだけで、確定日は未定です。既存の4系アプリも、Node.js 18以上へ上げたうえで移行を計画してください。

body-parserはインストールが必要?

不要です。Express 4.16以降、JSONとURLエンコードのパーサは本体に取り込まれており、express.json()express.urlencoded() で足ります。npm install body-parser から始まる解説は、それ以前の情報です。ただしファイルアップロード(multipart/form-data)だけは本体では扱えず、multer などが必要です。

req.bodyがundefinedになるのはなぜ?

Express 5では、ボディがパースされていない場合の req.body{} ではなく undefined です(4系からの変更点)。express.json() を登録していない、ルート定義より後ろに登録している、あるいはリクエストに Content-Type: application/json が付いていない、のいずれかを疑ってください。

ExpressでリアルタイムなプッシュやWebSocketは扱える?

Expressはリクエスト/レスポンス型のHTTPを前提としており、サーバーからの継続的なプッシュは守備範囲外です。サーバー起点の一方向通知だけならSSE(Server-Sent Events)で text/event-stream を返す実装がExpress上でも可能で、双方向通信が必要ならWebSocket(ws・Socket.IO)を併用します。選び分けはSSE(Server-Sent Events)とは|仕組み・WebSocketとの違い・実装を解説で整理しています。

関連記事

資料請求

RELATED POSTS 関連記事