Fastify の使い方 — 高速・低オーバーヘッドなNode.js Webフレームワーク
一言でいうと
Fastifyは、Node.js向けの高速かつ低オーバーヘッドなWebフレームワークです。JSON Schemaによるバリデーション/シリアライゼーションの自動最適化と、強力なプラグインアーキテクチャを特徴とし、Expressの約5倍のリクエスト処理性能を実現します。
どんな時に使う?
- 高スループットが求められるREST APIの構築 — マイクロサービスやBFF(Backend for Frontend)など、レスポンス速度がビジネスに直結する場面
- JSON Schemaベースでリクエスト/レスポンスを厳密に管理したいとき — スキーマ定義がそのままバリデーションとOpenAPIドキュメントの元になる
- プラグインで機能を段階的に拡張したいとき — 認証、CORS、データベース接続などをカプセル化されたプラグインとして管理し、大規模プロジェクトでも見通しの良い設計を維持できる
インストール
# npm
npm install fastify
# yarn
yarn add fastify
# pnpm
pnpm add fastify
本記事はFastify v5系(5.8.4)を対象としています。v4からの移行には破壊的変更があるため、公式のマイグレーションガイドを参照してください。
基本的な使い方
最もシンプルなHTTPサーバーの例です。
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
const app = Fastify({
logger: true, // Pinoベースの高速ロガーを有効化
});
// ルート定義(async/await)
app.get('/', async (request: FastifyRequest, reply: FastifyReply) => {
return { hello: 'world' };
});
// サーバー起動
const start = async () => {
try {
await app.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();
async ハンドラでは return した値が自動的にレスポンスボディになります。reply.send() を明示的に呼ぶ必要はありません。
よく使うAPIと使い方
1. JSON Schemaによるバリデーションとシリアライゼーション
Fastifyの最大の強みの一つです。ルートにスキーマを定義すると、リクエストの自動バリデーションとレスポンスの高速シリアライゼーションが有効になります。
import Fastify from 'fastify';
const app = Fastify({ logger: true });
const createUserSchema = {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
} as const;
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
const { name, email } = request.body as { name: string; email: string };
// DB保存処理(省略)
const user = { id: 'abc-123', name, email };
reply.code(201);
return user;
});
スキーマに合致しないリクエストは自動的に 400 Bad Request で返却されます。レスポンススキーマを定義すると、内部的に fast-json-stringify が使われ、JSON.stringify より2〜3倍高速にシリアライズされます。
2. プラグインシステム(register)
Fastifyの設計思想の根幹です。register でプラグインを登録すると、スコープがカプセル化されます。
import Fastify, { FastifyInstance, FastifyPluginOptions } from 'fastify';
const app = Fastify({ logger: true });
// プラグイン定義
async function userRoutes(
fastify: FastifyInstance,
opts: FastifyPluginOptions
) {
// このスコープ内でのみ有効なデコレータ
fastify.decorate('userService', {
findById: async (id: string) => ({ id, name: 'Alice' }),
});
fastify.get('/users/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const user = await (fastify as any).userService.findById(id);
return user;
});
}
// プレフィックス付きで登録
app.register(userRoutes, { prefix: '/api/v1' });
// → GET /api/v1/users/:id が有効になる
app.listen({ port: 3000 });
プラグイン内で追加したデコレータやフックは、そのスコープと子スコープにのみ影響します。これにより、大規模アプリケーションでも依存関係が明確に保たれます。
3. ライフサイクルフック(Hooks)
リクエスト処理の各段階に介入できます。
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// 全ルート共通の認証チェック
app.addHook('onRequest', async (request, reply) => {
const token = request.headers.authorization;
if (!token) {
reply.code(401).send({ error: 'Unauthorized' });
return; // ここでレスポンスが確定し、以降の処理はスキップされる
}
});
// レスポンス送信直前にヘッダーを追加
app.addHook('onSend', async (request, reply, payload) => {
reply.header('X-Response-Time', Date.now().toString());
return payload; // payloadを返す(変更も可能)
});
// レスポンス送信完了後のログ記録
app.addHook('onResponse', async (request, reply) => {
request.log.info(
{ statusCode: reply.statusCode, url: request.url },
'request completed'
);
});
app.get('/', async () => ({ status: 'ok' }));
app.listen({ port: 3000 });
主要なフックの実行順序は以下の通りです:
onRequest → preParsing → preValidation → preHandler → (handler) → preSerialization → onSend → onResponse
4. デコレータ(Decorators)
Fastifyインスタンスやリクエスト/リプライオブジェクトにカスタムプロパティやメソッドを追加できます。
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// Fastifyインスタンスへのデコレータ
app.decorate('config', {
dbHost: process.env.DB_HOST || 'localhost',
dbPort: parseInt(process.env.DB_PORT || '5432', 10),
});
// リクエストオブジェクトへのデコレータ
app.decorateRequest('userId', '');
app.addHook('preHandler', async (request) => {
// 認証トークンからユーザーIDを抽出(簡略化)
request.userId = 'user-42';
});
app.get('/profile', async (request, reply) => {
const userId = request.userId;
return { userId, dbHost: app.config.dbHost };
});
app.listen({ port: 3000 });
TypeScriptで型安全にデコレータを使うには、モジュール拡張(Declaration Merging)を活用します。
declare module 'fastify' {
interface FastifyInstance {
config: {
dbHost: string;
dbPort: number;
};
}
interface FastifyRequest {
userId: string;
}
}
5. エラーハンドリング(setErrorHandler)
アプリケーション全体のエラーハンドリングを一元管理できます。
import Fastify from 'fastify';
const app = Fastify({ logger: true });
// カスタムエラークラス
class NotFoundError extends Error {
statusCode = 404;
constructor(resource: string) {
super(`${resource} not found`);
}
}
// グローバルエラーハンドラ
app.setErrorHandler((error, request, reply) => {
request.log.error(error);
// バリデーションエラー(JSON Schemaによる自動バリデーション)
if (error.validation) {
reply.code(400).send({
error: 'Validation Error',
message: error.message,
details: error.validation,
});
return;
}
// カスタムエラー
const statusCode = (error as any).statusCode || 500;
reply.code(statusCode).send({
error: statusCode >= 500 ? 'Internal Server Error' : error.message,
});
});
// 404ハンドラ
app.setNotFoundHandler((request, reply) => {
reply.code(404).send({
error: 'Not Found',
message: `Route ${request.method} ${request.url} not found`,
});
});
app.get('/users/:id', async (request) => {
const { id } = request.params as { id: string };
if (id === '0') {
throw new NotFoundError('User');
}
return { id, name: 'Alice' };
});
app.listen({ port: 3000 });
類似パッケージとの比較
| 特徴 | Fastify | Express | Koa | Hapi |
|---|---|---|---|---|
| パフォーマンス(req/sec) | ~77,000 | ~14,200 | ~54,000 | ~42,000 |
| JSON Schemaバリデーション | 組み込み | 別途ミドルウェア必要 | 別途ミドルウェア必要 | Joi組み込み |
| プラグインカプセル化 | ◎(スコープ分離) | △(グローバル) | △(グローバル) | ○ |
| TypeScriptサポート | ◎(型定義同梱) | ○(@types必要) | ○(@types必要) | ○(型定義同梱) |
| ロガー | Pino組み込み | なし(morgan等を追加) | なし | 組み込み |
| エコシステム規模 | 大(成長中) | 最大 | 中 | 中 |
| 学習コスト | 中 | 低 | 低 | 高 |
選定の目安:
- Expressからの移行で性能改善したい → Fastify(APIの設計思想が近く移行しやすい)
- 既存のExpressミドルウェア資産が大量にある → Express継続 or
@fastify/expressで段階移行 - スキーマ駆動開発を重視 → Fastify(JSON Schemaとの統合が最も深い)
注意点・Tips
コンテナ環境では host: '0.0.0.0' を指定する
Fastifyはデフォルトで localhost(127.0.0.1)にバインドします。Docker/Kubernetes環境では外部からアクセスできないため、明示的に指定が必要です。
// ❌ コンテナ内で外部からアクセスできない
await app.listen({ port: 3000 });
// ✅ 全インターフェースでリッスン
await app.listen({ port: 3000, host: '0.0.0.0' });
async ハンドラでは reply.send() と return を混在させない
// ❌ 二重レスポンスになりエラーが発生する
app.get('/bad', async (request, reply) => {
reply.send({ a: 1 });
return { b: 2 };
});
// ✅ どちらか一方だけを使う
app.get('/good', async (request, reply) => {
return { a: 1 };
});
レスポンススキーマは「フィルタ」として機能する
レスポンススキーマに定義されていないプロパティは、レスポンスから自動的に除外されます。これはセキュリティ上の利点(パスワードハッシュ等の意図しない漏洩防止)になりますが、意図せずフィールドが消えるハマりポイントにもなります。
// スキーマに password が含まれていなければ、レスポンスから自動除外される
const schema = {
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
// password は定義しない → レスポンスに含まれない
},
},
},
};
プラグインの登録順序に注意
register は非同期で処理されますが、登録順序は保証されます。依存関係のあるプラグインは正しい順序で登録してください。
// ✅ DB接続プラグインを先に登録し、それに依存するルートを後に登録
app.register(dbPlugin);
app.register(userRoutes); // 内部で app.db を使う
v4 → v5 の主な破壊的変更
reply.send()のコール