Fastify: 高性能のNode.js Webフレームワーク
Fastifyは、最適なパフォーマンスを提供するように設計された、効率的で高速なNode.js Webフレームワークです。比較的新しいものですが、その高いパフォーマンスと低いオーバーヘッドのために多くの開発者に支持されています。Fastifyは簡潔な開発体験を提供し、高速ルーティングとプラグインアーキテクチャをサポートしており、開発者がアプリケーションを構築し、拡張するのが容易です。Fastify公式サイトを参考に、以下ではFastifyを様々な側面から紹介します。
1. 主な機能と原則
- 高いパフォーマンス:Fastifyは最も高速なWebフレームワークの一つです。コードの複雑さに応じて、1秒間に最大30,000件のリクエストを処理することができます。
- 拡張性:フック、プラグイン、デコレータを使って、Fastifyは完全に拡張可能です。
- スキーマベース:必須ではありませんが、ルートを検証し、出力をシリアライズするためにJSON Schemaを使用することが推奨されます。Fastifyはスキーマを高性能の関数にコンパイルします。
- ロギング:最良のロガーであるPinoを選択することで、ロギングのコストをほとんどゼロに近づけることができます。
- 開発者に優しい:このフレームワークは表現力が高く、開発者の日常的な使用に便利であり、パフォーマンスとセキュリティを犠牲にすることなく使用できます。
- TypeScriptに対応:成長し続けるTypeScriptコミュニティをサポートするために、TypeScriptの型宣言ファイルを維持するよう努めています。
2. 誕生の背景
Fastifyが登場する前は、ExpressとKoaはNode.js分野で広く使用されていた2つのフレームワークでした。これらは人気があり、使いやすいものの、大量のリクエストを処理する際には、そのパフォーマンスは最適ではありませんでした。Fastifyは以下の重要な問題を解決することを目的としています:
- パフォーマンス:各リクエストのオーバーヘッドを削減することで、既存のフレームワークよりも高いパフォーマンスを提供し、より多くのリクエストを処理できます。これは高性能なアプリケーションを構築する上で重要な要素です。
- 開発効率:プラグインアーキテクチャとそのまま使える機能(例えばスキーマ検証、ロギングなど)を使って、効率的で使いやすい開発環境を提供し、開発プロセスを加速し、潜在的なエラーを減らします。
- スキーマ検証:JSON Schemaに対する組み込みのサポートを備えており、これはクライアントの入力を検証するだけでなく、アプリケーションのデータの一貫性と信頼性を保証します。これは多くのフレームワークがコアレベルで統合していない機能です。
- 拡張性:プラグインアーキテクチャにより、開発者は新しい機能を簡単に追加したり、サードパーティのライブラリを統合したりでき、アプリケーションの全体的なパフォーマンスに大きな影響を与えることはありません。
上記の問題を解決することで、Fastifyは高性能で保守しやすく、開発しやすいNode.jsアプリケーションを開発するための強力なプラットフォームを提供し、JavaScriptコミュニティで急速に注目を集め、使用されるようになりました。
3. 高いパフォーマンスの実現
Fastifyの高いパフォーマンスの鍵は、その設計とアーキテクチャの選択にあり、これらは各リクエストのオーバーヘッドを最小限に抑える助けとなります:
- 効率的なルーティング配分:Fastifyは高速なルーティング配分メカニズムを採用しています。リクエストが来たとき、すぐに呼び出す処理関数を決定することができ、大幅にリクエスト処理時間を短縮します。
- 事前コンパイルされたシリアライゼーション:レスポンスを送信する前に、Fastifyは事前コンパイルされたシリアライゼーション関数を使用し、実行時にオブジェクトを動的にシリアライズするのではなく、これによりレスポンスのシリアライゼーションを高速化し、クライアントに送信します。
- スキーマ駆動型開発:Fastifyは(場合によっては必須となる)JSON Schemaを使用してルートの入力と出力を定義することを強く推奨しています。これはAPIを検証し、文書化する助けとなるだけでなく、Fastifyが検証ロジックを事前にコンパイルできるようにし、実行時の効率を向上させます。
- 最適化されたプラグインシステム:Fastifyのプラグインアーキテクチャは効率的に設計されており、コードの再利用とベストプラクティスをサポートしながら、コアフレームワークを軽量に保ち、パフォーマンスを犠牲にすることなく、高いレベルのモジューラリティと柔軟性を実現します。
- 効率的なロギング:組み込みのロギングツールであるPinoは速度を重視して設計されており、極めて低いオーバーヘッドでロギング機能を提供します。これはアプリケーションのパフォーマンスを維持する上で重要です。
- ゼロコストの抽象化:Fastifyの設計哲学は、抽象層のパフォーマンスオーバーヘッドを最小限に抑えることです。様々な抽象化や便利な機能を使用していても、高いパフォーマンスを維持することができます。
これらの設計決定と最適化により、Fastifyは優れたパフォーマンスを提供でき、効率的で応答性の高いNode.jsアプリケーションを構築するための理想的な選択肢です。
4. インストールと使用方法
4.1 npmを使ってFastifyをインストールする
npm install fastify
4.2 シンプルなサンプルコード
// フレームワークをインポートし、インスタンス化する
import Fastify from "fastify";
const fastify = Fastify({
logger: true,
});
// ルートを宣言する
fastify.get("/", async function handler(request, reply) {
return { hello: "world" };
});
// サーバーを実行する!
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
4.3 cliを使ってスケルトンを生成する
npm install --global fastify-cli
fastify generate newproject
4.4 JSON Schemaとフックを使ってリクエストを処理するサンプル
import Fastify from "fastify";
const fastify = Fastify({
logger: true,
});
fastify.route({
method: "GET",
url: "/",
schema: {
// リクエストには`name`パラメータを持つクエリ文字列が必要
querystring: {
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
// レスポンスは`hello`プロパティが`string`型のオブジェクトである必要がある
response: {
200: {
type: "object",
properties: {
hello: { type: "string" },
},
},
},
},
// この関数はハンドラーが実行される前に、すべてのリクエストに対して実行される
preHandler: async (request, reply) => {
// 例えば、認証をチェックする
},
handler: async (request, reply) => {
return { hello: "world" };
},
});
try {
await fastify.listen({ port: 3000 });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
5. プラグイン、デコレータ、ミドルウェア、フック
Fastifyにおいて、プラグイン、デコレータ、ミドルウェア、フックはフレームワークの核心概念であり、それぞれ異なる役割を果たします:
5.1 プラグイン
プラグインは、Fastifyアプリケーションに機能を追加し、コードを共有し、ロジックをカプセル化する主な方法です。プラグインは、Fastifyインスタンス、オプション、コールバック関数をパラメータとする関数とすることができます。プラグインはルートを登録し、デコレータを追加し、新しいフックを宣言し、さらに他のプラグインをカプセル化することもでき、モジュール化されたアプリケーションを構築するために使用されます。開発者はこれらを使って再利用可能なロジックブロックを構築し、異なるFastifyアプリケーションや、同じアプリケーションの異なる部分で共有することができます。
5.2 デコレータ
デコレータは、Fastifyインスタンス、リクエスト(Request)、レスポンス(Reply)オブジェクトを拡張するために使用されます。新しいメソッドやプロパティを追加することで、開発者はカスタムの機能やデータを追加し、アプリケーションの異なる部分で利用できるようにすることができます。例えば、デコレータを追加して、各リクエストに対して共有設定データやサービスにアクセスするメソッドを追加することができます。
5.3 ミドルウェア
Fastifyはミドルウェアに依存するように設計されていませんが、Express/Connectスタイルのミドルウェアの使用をサポートしており、主に互換性や特定の機能の統合のために使用されます。ミドルウェアはリクエストとレスポンスオブジェクトにアクセスし、コードを実行し、リクエストとレスポンスオブジェクトを変更し、リクエスト処理チェーンを終了したり、呼び出しスタック内の次のミドルウェアを呼び出したりすることができます。Fastifyのミドルウェアを使用する際には注意が必要で、不適切な使用はFastifyのいくつかの最適化をバイパスし、パフォーマンスに影響を与える可能性があります。
5.4 フック
フックは、Fastifyにおける機構であり、開発者がリクエストのライフサイクルの異なる段階(例えば、リクエストが受け取られた後、ルートが解決される前、レスポンスが送信される前など)で介入し、ロジックを実行することを可能にします。フックは前処理や後処理のロジックを実行するために使用され、例えば、権限チェック、リクエストロギング、レスポンスの変更を行うことができます。以下に続きます。
5.4 フック(続き)
更を行うことができます。以下に続きます。
5.4 フック(続き)
フックは、Fastifyにおける機構であり、開発者がリクエストのライフサイクルの異なる段階(例えば、リクエストが受け取られた後、ルートが解決される前、レスポンスが送信される前など)で介入し、ロジックを実行することを可能にします。フックは前処理や後処理のロジックを実行するために使用され、例えば、権限チェック、リクエストロギング、レスポンスの変更などに用いられます。Fastifyは様々な種類のフック(例えば onRequest、preHandler、onSend など)を提供しており、開発者はリクエスト処理の様々な段階を細かく制御することができます。
これらのコンポーネントが協働することで、Fastifyには大きな柔軟性と拡張性がもたらされ、フレームワークの高性能な特性を維持しながら動作します。
5.5 ライフサイクルの重み
Fastifyのコンポーネント(プラグイン、デコレータ、ミドルウェア、フック)は、特定の実行順序と優先順位を持っており、これはリクエスト処理フローにおけるそれらの作用のタイミングを決定します。この実行フローを理解することは、効率的で信頼性の高いFastifyアプリケーションを設計する上で重要です:
- プラグイン:アプリケーションが起動するときに読み込まれ、登録された順序で実行されます。アプリケーションが起動した後、プラグインの設定は固定され、プラグイン内で定義された機能(例えばルート、フック、デコレータ)は、その後の各リクエストに対して使用することができます。
- デコレータ:それらには明確な実行時刻はありません。デコレートされたオブジェクト(Fastifyインスタンス、リクエスト、レスポンス)に追加されたメソッドやプロパティはすぐに利用可能であり、オブジェクトのライフサイクル全体を通じて有効なままです。
- ミドルウェア:各リクエストの処理フローの初期段階で、具体的にはルートマッチングの前に実行されます。ミドルウェアはリクエストとレスポンスオブジェクトを変更したり、リクエストを次のプロセッサに渡すかどうかを決定したりすることができます。
-
フック:特定の実行順序を辿り、これはリクエスト処理フローに反映されます:
- onRequest:リクエストが受け取られた直後、他のどの処理よりも前に実行されます。
- preParsing:リクエストボディが解析される前に実行されます。
- preValidation:ルートレベルの検証の前に実行されます。
- preHandler:ルート処理関数の前に実行されます。ここでは権限チェックなどの操作を行うことができます。
- preSerialization:レスポンスがシリアライズされる前、つまりクライアントに送信される前に実行されます。
- onSend:レスポンスがクライアントに送信される前に、シリアライズの後に実行されます。
- onResponse:レスポンスが完全にクライアントに送信された後に実行されます。
中断する能力
- プラグイン:直接的にはリクエスト処理の中断に関与しませんが、そのプロセスに影響を与えるフックやミドルウェアを登録することができます。
- デコレータ:プロセスを制御しませんので、中断に関与しません。
- ミドルウェア:リクエスト処理フローを中断することができます。例えば、next() を呼び出さないか、レスポンスを送信することで、その後の処理を停止することができます。
- フック:特定のフック(例えば preHandler)は、リクエストの処理を続行するか、直接レスポンスを送信するかを決定することができ、その結果、その後のプロセスを中断することができます。
これらのコンポーネントの実行順序と中断能力を理解することは、期待通りに動作するFastifyアプリケーションを構築する上で重要です。
6. ライフサイクル
通常のプロセス
- [着信リクエスト]:新しいリクエストがシステムに入ります。
- ↓
- [ルーティング]:リクエストに対応するルートを決定します。
- ↓
- [インスタンスロガー]:リクエストに関連するインスタンスログを記録します。
- ↓
-
[onRequestフック]:
onRequest
フック関数を実行します。これは一括したリクエスト前処理に使用することができます。 - ↓
- [preParsingフック]:リクエストボディが解析される前にフック関数を実行します。
- ↓
- [解析]:リクエストボディを解析します。
- ↓
- [preValidationフック]:ルート検証の前にフック関数を実行します。
- ↓
-
{検証}:ルート検証を実行します:
- 検証に失敗した場合→[400]:400エラーレスポンスを返します。
- 検証に合格した場合→↓
- [preHandlerフック]:ルート処理関数の前にフック関数を実行します。権限チェックなどの操作を行うことができます。
- ↓
- [ユーザーハンドラー]:ユーザー定義のルート処理関数を実行します。
- ↓
- [レスポンス]:レスポンス内容を準備します。
- ↓
- [preSerializationフック]:レスポンスがシリアライズされる前にフック関数を実行します。
- ↓
- [onSendフック]:レスポンスが送信される前にフック関数を実行します。
- ↓
- [発信レスポンス]:レスポンスを送信します。
- ↓
- [onResponseフック]:レスポンスが完全にクライアントに送信された後にフック関数を実行します。
7. カプセル化
Fastifyの encapsulation context
(カプセル化コンテキスト)は、その基本的な機能の一つです。カプセル化コンテキストは、どのデコレータ、登録されたフック、プラグインがルートで使用可能かを決定します。各コンテキストは親コンテキストからのみ継承され、親コンテキストはその子孫コンテキスト内のどんなエンティティにもアクセスすることができません。デフォルトの状況が要件を満たさない場合、fastify-pluginを使用してカプセル化コンテキストを破ることができ、子孫コンテキストで登録されたコンテンツを含む親コンテキストで利用可能にすることができます。
8. ルートのJSON Schema検証
FastifyではJSON Schemaがルートと組み合わされ、クライアントのリクエストデータを検証し、レスポンスデータをフォーマットします。JSON Schemaを定義することで、入力されるリクエストデータが特定の形式と型の要件を満たすことを保証することができ、同時にレスポンスデータの構造を制御し、APIのロバスト性とセキュリティを向上させます。
サンプルコード
const fastify = require("fastify")();
// JSON Schemaを定義する
const userSchema = {
type: "object",
required: ["username", "email", "gender"],
properties: {
username: { type: "string" },
email: { type: "string", format: "email" },
gender: { type: "string", minimum: "male" },
},
};
fastify.route({
method: "POST",
url: "/create-user",
schema: {
body: userSchema, // JSON Schemaを使用してリクエストボディを検証する
response: {
200: {
type: "object",
properties: {
success: { type: "boolean" },
id: { type: "string" },
},
},
},
},
handler: (request, reply) => {
// リクエスト処理ロジック
// ユーザー作成が成功し、ユーザーIDを返すと仮定する
reply.send({ success: true, id: "leapcell" });
},
});
fastify.listen(3000, (err) => {
if (err) throw err;
console.log("Server is running on http://localhost:3000");
});
この例では:
-
userSchema
が定義されており、リクエストボディの期待される形式を記述しており、必要なフィールドと各フィールドの型を含んでいます。 - ルート設定では、
schema
プロパティを通じてuserSchema
がリクエストボディ(body
)に適用され、Fastifyは自動的に入力されるリクエストデータが定義されたスキーマに合致するかどうかを検証します。 - レスポンスの
schema
が定義されており、レスポンスデータの構造が期待されるものに合致することを保証します。
このようにして、すべての入力されるリクエストが厳密に検証され、すべてのレスポンスが予定された形式に合致することが保証され、APIのロバスト性とユーザーにとっての予測可能性が向上します。
9. サンプル
9.1 ログイン状態の問題を処理するフック
const fastify = require("fastify")({ logger: true });
const secureSession = require("fastify-secure-session");
// セッションプラグインを登録する
fastify.register(secureSession, {
secret: "averylognsecretkey", // 長く複雑なシークレットキーを使用する必要がある
cookie: {
path: "/",
// 必要に応じて他のクッキーオプションを設定する
},
});
// preHandlerフックを/api/aと/api/cにのみ適用する
fastify.addHook("preHandler", (request, reply, done) => {
if (
(request.routerPath === "/api/1" || request.routerPath === "/api/2") &&
!request.session.user
) {
request.session.redirectTo = request.raw.url;
reply.redirect(302, "/login");
done();
} else {
done();
}
});
// セッション検証が不要なルート
fastify.get("/api/2", (req, reply) => {
reply.send({ message: "Access granted for /api/2" });
});
// セッション検証が必要なルート
fastify.get("/api/1", (req, reply) => {
reply.send({ message: "Access granted for /api/1" });
});
fastify.get("/api/3", (req, reply) => {
reply.send({ message: "Access granted for /api/3" });
});
// ログインページのルート
fastify.get("/login", (req, reply) => {
reply.send(`Login form goes here. POST to /login to authenticate.`);
});
// ログインロジック
fastify.post("/login", (req, reply) => {
const { username, password } = req.body;
if (username === "user" && password === "password") {
req.session.user = username;
const redirectTo =const redirectTo = req.session.redirectTo || "/";
delete req.session.redirectTo;
reply.redirect(302, redirectTo);
} else {
reply.code(401).send({ error: "Unauthorized" });
}
});
// サーバーを起動する
fastify.listen(3000, (err) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
console.log(`Server listening on http://localhost:3000`);
});
9.2 パラメータの制限
次の例では、POSTリクエストにおいてa
とb
のパラメータが必ず渡されることが要求されます:
const fastify = require("fastify")();
const postSchema = {
schema: {
body: {
type: "object",
required: ["a", "b"],
properties: {
a: { type: "string" },
b: { type: "string" }
}
},
response: {
// 欠落した場合、デフォルトで400エラーを返し、ここで400の戻り値の型を変更できます
400: {
type: "object",
properties: {
statusCode: { type: "number" },
error: { type: "string" },
message: { type: "string" }
}
}
},
// ここで上記のデフォルトのインターセプト戦略を遮断できます
preValidation: (request, reply, done) => {
if (!request.body ||!request.body.a ||!request.body.b) {
reply
.code(400)
.send({ error: "Bad Request", message: "Missing required parameters" });
return;
}
done();
}
};
fastify.post("/api/data", postSchema, (request, reply) => {
// ここに到達した場合、aとbのパラメータがどちらも検証を通過していることを意味します
reply.send({ message: "Success" });
});
fastify.listen(3000, (err) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log("Server listening on port 3000");
});
もし、ルートでpreValidation
フックとカスタムのresponse schema
を定義しない場合、POSTリクエストのbody
が定義されたJSON Schemaに合致しないとき、Fastifyはデフォルトで400エラー(Bad Request)を返します。このエラーのレスポンスボディには、どのフィールドが要件を満たしていないかに関する情報が含まれますが、この情報はFastifyによって自動生成されるもので、カスタムエラーメッセージほど具体的で明確であるとは限りません。デフォルトでは、a
パラメータのみが渡され、b
パラメータが欠落している場合、Fastifyはエラー詳細を含むレスポンスを返し、以下のようになります:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'b'"
}
Leapcell: サーバレスWebホスティングのベスト
最後に、NodeJSサービスをデプロイするのに最適なプラットフォームをお勧めします:Leapcell
🚀 好きな言語で構築しましょう
JavaScript、Python、Go、またはRustで楽に開発できます。
🌍 無制限のプロジェクトを無料でデプロイしましょう
使用した分だけ支払います — リクエストがなければ、請求はありません。
⚡ 使った分だけ支払い、隠された費用はありません
アイドル料はなく、シームレスにスケーラブルです。
🔹 Twitterでフォローしましょう: @LeapcellHQ