Express vs Fastify — Node.js Webフレームワーク徹底比較
1. 結論
新規プロジェクトでパフォーマンスと型安全性を重視するなら Fastify を選びましょう。 既存の膨大なミドルウェア資産を活用したい場合や、チームの学習コストを最小限にしたい場合は Express が依然として堅実な選択です。どちらも本番運用に十分耐えうるフレームワークですが、設計思想が異なるため、プロジェクトの要件に合わせて選ぶことが重要です。
2. 比較表
| 項目 | Express | Fastify |
|---|---|---|
| npm 週間DL数 | 約 3,500万+ | 約 350万+ |
| GitHub Stars | 約 66k+ | 約 33k+ |
| 最新メジャーバージョン | v4(v5 beta) | v5 |
| バンドルサイズ(node_modules) | 約 210 KB | 約 2.2 MB(プラグイン込み) |
| ベンチマーク(req/sec) | 約 15,000 | 約 50,000+ |
| TypeScript 対応 | @types/express で外部型定義 | コア同梱の型定義(ファーストクラス) |
| スキーマバリデーション | 外部ライブラリが必要 | JSON Schema ベースで組み込み |
| ロギング | 外部ライブラリが必要 | Pino が組み込み |
| プラグインエコシステム | 非常に豊富(数千) | 成長中(公式プラグイン 200+) |
| 学習コスト | 低い | やや高い(プラグインシステムの理解が必要) |
| 設計思想 | ミニマル・非主張的 | プラグイン駆動・カプセル化 |
| HTTP/2 対応 | 外部モジュールが必要 | ネイティブサポート |
| ライセンス | MIT | MIT |
3. それぞれの強み
Express の強み
- 圧倒的なエコシステム: 10年以上の歴史があり、ほぼあらゆるユースケースに対応するミドルウェアが存在します。Passport.js、multer、cors など、定番ライブラリとの統合がスムーズです。
- 学習コストの低さ:
req/res/nextというシンプルなミドルウェアモデルは、Node.js 初学者でも直感的に理解できます。ドキュメント、書籍、チュートリアルの量も圧倒的です。 - 柔軟性: フレームワーク側が構造を強制しないため、小さな API サーバーからモノリシックなアプリケーションまで自由に設計できます。
- 採用実績: Netflix、Uber、IBM など大規模企業での採用実績があり、長期的なメンテナンスへの信頼感があります。
Fastify の強み
- 圧倒的なパフォーマンス: 内部で
find-my-way(Radix Tree ベースのルーター)と高速な JSON シリアライゼーションを採用しており、Express の約 3〜5 倍のスループットを実現します。 - ファーストクラスの TypeScript サポート: ジェネリクスを活用した型定義がコアに同梱されており、リクエスト・レスポンスの型を厳密に定義できます。
- JSON Schema によるバリデーション & シリアライゼーション: ルート定義にスキーマを宣言するだけで、入力バリデーションとレスポンスのシリアライゼーション高速化を同時に実現します。
- プラグインアーキテクチャ: カプセル化されたプラグインシステムにより、依存関係の分離とテスタビリティが向上します。
fastify-plugin、@fastify/autoloadなどの仕組みが強力です。 - 組み込みロギング: 高速ロガー Pino が統合されており、構造化ログを追加設定なしで利用できます。
4. コード例で比較
ユーザー作成 API(POST /users)を実装する
Express の場合
// express-app.ts
import express, { Request, Response, NextFunction } from "express";
interface CreateUserBody {
name: string;
email: string;
age: number;
}
interface User extends CreateUserBody {
id: number;
}
const app = express();
app.use(express.json());
// ── バリデーションミドルウェア(手動実装) ──
function validateCreateUser(req: Request, res: Response, next: NextFunction): void {
const { name, email, age } = req.body as Partial<CreateUserBody>;
const errors: string[] = [];
if (typeof name !== "string" || name.length < 1) {
errors.push("name は1文字以上の文字列が必要です");
}
if (typeof email !== "string" || !email.includes("@")) {
errors.push("email は有効なメールアドレスが必要です");
}
if (typeof age !== "number" || age < 0 || age > 150) {
errors.push("age は 0〜150 の数値が必要です");
}
if (errors.length > 0) {
res.status(400).json({ errors });
return;
}
next();
}
// ── ルート定義 ──
app.post("/users", validateCreateUser, (req: Request, res: Response) => {
const { name, email, age } = req.body as CreateUserBody;
const newUser: User = {
id: Math.floor(Math.random() * 10000),
name,
email,
age,
};
res.status(201).json(newUser);
});
// ── エラーハンドリング ──
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: "Internal Server Error" });
});
app.listen(3000, () => {
console.log("Express server running on http://localhost:3000");
});
Fastify の場合
// fastify-app.ts
import Fastify, { FastifyRequest, FastifyReply } from "fastify";
interface CreateUserBody {
name: string;
email: string;
age: number;
}
interface User extends CreateUserBody {
id: number;
}
const app = Fastify({
logger: true, // Pino ロガーが自動で有効化
});
// ── ルート定義(スキーマバリデーション込み) ──
app.post<{ Body: CreateUserBody; Reply: User }>(
"/users",
{
schema: {
body: {
type: "object",
required: ["name", "email", "age"],
properties: {
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" },
age: { type: "integer", minimum: 0, maximum: 150 },
},
additionalProperties: false,
},
response: {
201: {
type: "object",
properties: {
id: { type: "integer" },
name: { type: "string" },
email: { type: "string" },
age: { type: "integer" },
},
},
},
},
},
async (request: FastifyRequest<{ Body: CreateUserBody }>, reply: FastifyReply) => {
const { name, email, age } = request.body;
const newUser: User = {
id: Math.floor(Math.random() * 10000),
name,
email,
age,
};
return reply.status(201).send(newUser);
}
);
// ── サーバー起動 ──
app.listen({ port: 3000 }, (err) => {
if (err) {
app.log.error(err);
process.exit(1);
}
});
コード例から読み取れる違い
| 観点 | Express | Fastify |
|---|---|---|
| バリデーション | ミドルウェアとして手動実装(または zod / joi を別途導入) | JSON Schema をルート定義に宣言するだけ |
| 型の連携 | req.body の型は any ベースでキャストが必要 | ジェネリクスで Body / Reply を型パラメータとして渡せる |
| ロギング | console.log か外部ライブラリを別途設定 | logger: true だけで構造化ログが出力される |
| レスポンスシリアライゼーション | JSON.stringify がそのまま使われる | レスポンススキーマから高速シリアライザが自動生成される |
| コード量 | バリデーション込みでやや多い | スキーマ定義は増えるが、ロジック部分は簡潔 |
5. どちらを選ぶべきか — ユースケース別の推奨
Express を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| プロトタイプ・MVP の高速開発 | 最小限のコードで動くものを作れる。情報量が多く詰まりにくい |
| 既存の Express アプリの保守・拡張 | 無理に移行するメリットは薄い。段階的な改善が現実的 |
| チームに Node.js 初心者が多い | ミドルウェアモデルはシンプルで教育コストが低い |
| 特定のミドルウェアに依存している | Passport.js、express-session など Express 専用のエコシステムが必要な場合 |
| SSR フレームワークとの統合 | Next.js のカスタムサーバーなど、Express 前提のツールが多い |
Fastify を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| 高トラフィックな API サーバー | ベンチマークで 3〜5 倍のスループット差は、インフラコストに直結する |
| TypeScript ファーストの新規プロジェクト | 型定義がコアに統合されており、DX が優れている |
| マイクロサービスアーキテクチャ | プラグインのカプセル化により、サービス境界の分離が自然にできる |
| OpenAPI / Swagger ドキュメント自動生成 | @fastify/swagger でスキーマからドキュメントを自動生成できる |
| 厳密な入出力バリデーションが必要 | JSON Schema ベースのバリデーションが組み込みで、セキュリティ面でも有利 |
| 構造化ログ・可観測性を重視 | Pino 統合により、JSON ログの出力が標準で行える |
どちらでもよいケース
- 小〜中規模の社内ツール API
- BFF(Backend for Frontend)レイヤー
- 学習目的の個人プロジェクト
これらの場合は、チームの習熟度や好みで選んで問題ありません。
6. まとめ
Express = 「枯れた安定」 × 「巨大なエコシステム」 × 「低い学習コスト」
Fastify = 「高パフォーマンス」 × 「型安全性」 × 「モダンな設計思想」
Express は Node.js Web フレームワークのデファクトスタンダードとして、今なお多くのプロジェクトで活躍しています。一方で Fastify は、Express の課題(パフォーマンス、型安全性、構造化)を正面から解決するために設計されたフレームワークです。
2025年時点での筆者の所感としては、新規の API サーバーを TypeScript で構築するなら Fastify を第一候補にすることをおすすめします。 ただし、Express のエコシステムの厚みは唯一無二であり、「Express だから悪い」ということは決してありません。
最終的には、チームのスキルセット・プロジェクトの寿命・パフォーマンス要件の 3 軸で判断するのが最も合理的です。どちらを選んでも、Node.js の Web 開発を支える優れたフレームワークであることに変わりはありません。