Fastifyを使ってみよう - 高速なNode.js APIサーバー入門

intermediate | 60分 で読める | 2026.04.10

公式ドキュメント

このチュートリアルで学ぶこと

  • 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 を検討: 高トラフィック時のログ量を制御

次のステップ(発展課題)

  1. DB接続: @fastify/postgres@fastify/mongodb で永続化
  2. 認証: @fastify/jwt でJWT認証を追加
  3. OpenAPI: @fastify/swagger でドキュメント自動生成
  4. CORS: @fastify/cors を導入
  5. Rate Limit: @fastify/rate-limit でAPI保護
  6. WebSocket: @fastify/websocket でリアルタイム通信
  7. Dockerコンテナ化して本番デプロイ

まとめ

Fastifyは速度・型安全・拡張性を高いレベルで両立したフレームワークです。

  • JSON Schema駆動でバリデーションとレスポンス最適化が同時に得られる
  • プラグインベースなので機能追加が安全
  • 公式エコシステムが豊富で、必要なものはたいてい揃う
  • TypeScript対応がファーストクラス

「Express + α」から始めたいNode.js開発者の次の一手として、Fastifyは最有力候補です。

参考リソース

← 一覧に戻る
PR
PR
PR
PR