この記事の要点
• FastifyはExpressの2倍以上高速なNode.js Webフレームワーク
• JSON Schema駆動でバリデーションとレスポンスシリアライズを自動最適化
• プラグインアーキテクチャで機能を安全に分離・拡張できる
このチュートリアルで学ぶこと
- Fastifyプロジェクトのセットアップ
- ルーティングとリクエスト/レスポンス
- JSON Schemaによる入力バリデーション
- プラグインアーキテクチャの理解
- エラーハンドリング
- ロギングとライフサイクルフック
- ユニットテスト(node:test)
FastifyはNode.js向けの低オーバーヘッド・高スループットなWebフレームワークです。Expressに比べてベンチマークで2倍以上の速度を出しつつ、JSON Schemaベースのバリデーションやプラグインシステムなどモダンな機能を備えています。
前提条件・必要な環境
- Node.js 20以上
- npm 10以上
- JavaScriptもしくはTypeScriptの基礎
- ターミナルとREST APIテストツール(curl / Insomnia / VS Code REST Client)
インストール / セットアップ
mkdir fastify-todo && cd fastify-todo
npm init -y
npm install fastify
npm install -D typescript @types/node tsx
npx tsc --init
package.json にスクリプトを追加します。
{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "node --test"
},
"type": "module"
}
tsconfig.json の主要設定:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
基本概念
Fastifyインスタンス
fastify() で作成。ルート登録、プラグイン登録、起動を行うコアオブジェクト。
プラグイン
ほぼ全ての機能はプラグインとして提供されます。fastify.register(plugin, opts) でツリー構造に組み込めます。
スキーマバリデーション
リクエストのbody / query / params / headers / responseに対しJSON Schemaを宣言でき、内部的にAjvで高速バリデーションされます。
ライフサイクルフック
onRequest preHandler onSend onResponse などのフックでミドルウェア的処理を挿入できます。
ステップバイステップ実装
シンプルなTodo APIを作っていきましょう。
実践メモ: tsx watchを使うとファイル変更時に自動リロードされ、開発効率が大幅に上がります。
Step 1: 最小サーバー
src/server.ts:
import Fastify from 'fastify';
const fastify = Fastify({
logger: {
level: 'info',
},
});
fastify.get('/health', async () => {
return { status: 'ok' };
});
const start = async () => {
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
起動:
npm run dev
curl http://localhost:3000/health
# => {"status":"ok"}
Step 2: Todoモデルとインメモリストア
src/store.ts:
import { randomUUID } from 'crypto';
export interface Todo {
id: string;
title: string;
done: boolean;
createdAt: string;
}
const todos = new Map<string, Todo>();
export const todoStore = {
list(): Todo[] {
return Array.from(todos.values());
},
get(id: string): Todo | undefined {
return todos.get(id);
},
create(title: string): Todo {
const todo: Todo = {
id: randomUUID(),
title,
done: false,
createdAt: new Date().toISOString(),
};
todos.set(todo.id, todo);
return todo;
},
update(id: string, patch: Partial<Pick<Todo, 'title' | 'done'>>): Todo | undefined {
const todo = todos.get(id);
if (!todo) return undefined;
Object.assign(todo, patch);
return todo;
},
remove(id: string): boolean {
return todos.delete(id);
},
};
ポイント: データストアをサーバーから分離しておくと、後からDBに差し替える際の変更が最小限で済みます。
Step 3: ルートをプラグイン化
src/routes/todos.ts:
import type { FastifyPluginAsync } from 'fastify';
import { todoStore } from '../store.js';
const todoRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/todos', async () => {
return todoStore.list();
});
fastify.get<{ Params: { id: string } }>(
'/todos/:id',
{
schema: {
params: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id'],
},
},
},
async (request, reply) => {
const todo = todoStore.get(request.params.id);
if (!todo) {
return reply.code(404).send({ error: 'Not Found' });
}
return todo;
},
);
fastify.post<{ Body: { title: string } }>(
'/todos',
{
schema: {
body: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string', minLength: 1, maxLength: 100 },
},
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
done: { type: 'boolean' },
createdAt: { type: 'string' },
},
},
},
},
},
async (request, reply) => {
const todo = todoStore.create(request.body.title);
return reply.code(201).send(todo);
},
);
fastify.patch<{
Params: { id: string };
Body: { title?: string; done?: boolean };
}>(
'/todos/:id',
{
schema: {
body: {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
done: { type: 'boolean' },
},
additionalProperties: false,
},
},
},
async (request, reply) => {
const updated = todoStore.update(request.params.id, request.body);
if (!updated) {
return reply.code(404).send({ error: 'Not Found' });
}
return updated;
},
);
fastify.delete<{ Params: { id: string } }>('/todos/:id', async (request, reply) => {
const ok = todoStore.remove(request.params.id);
if (!ok) {
return reply.code(404).send({ error: 'Not Found' });
}
return reply.code(204).send();
});
};
export default todoRoutes;
Step 4: ルートを登録
src/server.ts を更新:
import Fastify from 'fastify';
import todoRoutes from './routes/todos.js';
const fastify = Fastify({ logger: true });
fastify.get('/health', async () => ({ status: 'ok' }));
await fastify.register(todoRoutes, { prefix: '/api' });
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
Step 5: 動作確認
curl -X POST http://localhost:3000/api/todos \
-H 'Content-Type: application/json' \
-d '{"title":"Fastifyを学ぶ"}'
curl http://localhost:3000/api/todos
curl -X PATCH http://localhost:3000/api/todos/<id> \
-H 'Content-Type: application/json' \
-d '{"done":true}'
ポイント: スキーマを定義しておけば、不正リクエストはAjvで自動的に400エラーになります。バリデーションコードを書く必要がありません。
curl -X POST http://localhost:3000/api/todos -H 'Content-Type: application/json' -d '{}'
# => 400: body must have required property 'title'
Step 6: グローバルエラーハンドラ
src/server.ts に追加:
fastify.setErrorHandler((error, request, reply) => {
request.log.error({ err: error }, 'request errored');
if (error.validation) {
return reply.code(400).send({ error: 'Bad Request', message: error.message });
}
return reply.code(error.statusCode ?? 500).send({
error: error.name,
message: error.message,
});
});
Step 7: フック例 - リクエストログ
fastify.addHook('onRequest', async (request) => {
request.log.info({ url: request.url, method: request.method }, 'incoming');
});
注意: additionalProperties: falseをスキーマに指定すると、未定義のプロパティがエラーになります。APIの仕様に合わせて慎重に設定してください。
Step 8: テストを書く
test/todos.test.ts:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import Fastify from 'fastify';
import todoRoutes from '../src/routes/todos.js';
async function buildApp() {
const app = Fastify();
await app.register(todoRoutes, { prefix: '/api' });
return app;
}
test('creates and lists todos', async () => {
const app = await buildApp();
const created = await app.inject({
method: 'POST',
url: '/api/todos',
payload: { title: 'first' },
});
assert.equal(created.statusCode, 201);
const todo = created.json();
assert.equal(todo.title, 'first');
const list = await app.inject({ method: 'GET', url: '/api/todos' });
assert.equal(list.statusCode, 200);
assert.equal(list.json().length, 1);
await app.close();
});
test('rejects empty title', async () => {
const app = await buildApp();
const res = await app.inject({
method: 'POST',
url: '/api/todos',
payload: { title: '' },
});
assert.equal(res.statusCode, 400);
await app.close();
});
実行:
npm test
完成コード全体
fastify-todo/
├── src/
│ ├── routes/
│ │ └── todos.ts
│ ├── store.ts
│ └── server.ts
├── test/
│ └── todos.test.ts
├── package.json
└── tsconfig.json
実践メモ: app.inject()を使ったテストはHTTPサーバーを立てずに超高速で検証できます。CIでのテスト時間を大幅に短縮できます。
よくあるエラーと対処
FST_ERR_VALIDATION が連発する
スキーマと実際のbodyが食い違っています。additionalProperties: false を指定していると未定義のキーがエラーになるので注意。
起動時に EADDRINUSE
ポート3000が既に使われています。別のポートを指定するか、占有プロセスを終了させましょう。
lsof -i :3000
await fastify.register(...) の順序
プラグインは登録順に評価されます。依存があるプラグインは順序に気をつけて登録してください。
TypeScriptで型が効かない
ジェネリクス Body Params Querystring を明示するか、@fastify/type-provider-typebox などの型プロバイダを使うとスキーマから型推論できます。
ベストプラクティス
- スキーマを必ず書く: バリデーションだけでなく、レスポンスシリアライズも高速化する
- プラグインの単位を細かく: 機能ごとに分けてencapsulationを活用
- ログはpinoをそのまま使う: Fastify標準でpino統合済み。本番ではJSONログとして集約しやすい
- テストはinjectで書く: HTTPサーバーを立てずに高速テスト
- 環境変数は @fastify/env で管理: 型付きで安全
- 本番では
disableRequestLoggingを検討: 高トラフィック時のログ量を制御
次のステップ(発展課題)
- DB接続:
@fastify/postgresや@fastify/mongodbで永続化 - 認証:
@fastify/jwtでJWT認証を追加 - OpenAPI:
@fastify/swaggerでドキュメント自動生成 - CORS:
@fastify/corsを導入 - Rate Limit:
@fastify/rate-limitでAPI保護 - WebSocket:
@fastify/websocketでリアルタイム通信 - Dockerコンテナ化して本番デプロイ
まとめ
Fastifyは速度・型安全・拡張性を高いレベルで両立したフレームワークです。
- JSON Schema駆動でバリデーションとレスポンス最適化が同時に得られる
- プラグインベースなので機能追加が安全
- 公式エコシステムが豊富で、必要なものはたいてい揃う
- TypeScript対応がファーストクラス
「Express + α」から始めたいNode.js開発者の次の一手として、Fastifyは最有力候補です。
参考リソース
- Fastify Documentation
- Fastify GitHub Repository
- Fastify Getting Started Guide
- Fastify Ecosystem Plugins