Fastifyのベストプラクティス #2: autoLoad
どのようなWebフレームワークで開発するにせよ、プロジェクトの規模が拡大するにつれて、コード、特にルーティングをいかに効率的かつ明確に整理するかは、中心的な課題となります。優れた自動設定プロセスは、私たちの開発体験とプロジェクトの保守性を大幅に向上させることができます。
課題:手動登録の煩わしさ
FastifyでAPIルートを追加するのは非常に簡単です。
// index.ts
import fastify from 'fastify';
const server = fastify();
// 新しいAPIを追加
server.get('/ping', async (request, reply) => {
return 'pong\n';
});
// サーバーを起動...
プロジェクトが単一のファイルで構成されている場合、これは非常にシンプルに見えます。しかし、APIが徐々に増えていくと、コードの整理を保つために、各ルートのロジックを独立したファイルに分割したくなるのが自然です。その結果、コードの構造は次のようになります。
// routes/ping.ts
import { FastifyInstance, FastifyPluginOptions } from 'fastify';
// ルートをFastifyプラグインにカプセル化するのは良い実践方法
export default async (fastify: FastifyInstance, opts: FastifyPluginOptions) => {
fastify.get('/ping', async (request, reply) => {
return 'pong\n';
});
};
// index.ts
import pingRoute from './routes/ping';
import fastify from 'fastify';
const server = fastify();
// ルートファイルを追加するたびに、ここで手動で登録する必要がある
server.register(pingRoute);
// server.register(userRoute);
// server.register(productRoute);
// ... さらに多くのルート
この時点で、2つの明らかな問題に気づくでしょう。
- 手動メンテナンスの負担:新しいルートファイルを作成するたびに、メインのエントリーファイル(
index.ts)で手動でimportし、registerすることを覚えておく必要があります。これは面倒なだけでなく、忘れやすい作業です。 - 暗黙的な重複:ルートファイルのパス(例:
routes/ping.ts)と、それが定義するAPIエンドポイント(/ping)は、意味的に重複しています。
ファイル構造を直接APIパスにマッピングさせることはできないのでしょうか?これらの問題を解決するために、Fastifyの公式エコシステムは強力なツールを提供しています。それが@fastify/autoloadです。
解決策:@fastify/autoloadによる自動化
@fastify/autoloadは、特定のディレクトリを指定することで、そのディレクトリ内のすべてのファイルを自動的にスキャンし、プラグイン(ルートを含む)としてFastifyインスタンスに登録してくれます。
1. ルートの自動ロード
以下のAPIを定義する必要があるとします。
POST /management/user
DELETE /management/user/{id}
GET /user
GET /user/{id}
対応するファイルとディレクトリ構造を作成することで、autoloadにこれらのルートを自動生成させることができます。規約として、{id}のようなパスパラメータは、アンダースコアを接頭辞とするディレクトリまたはファイル名で表現する必要があります(例:_id)。
ディレクトリ構造は次のように設計できます。
src
┣ routes
┃ ┣ management
┃ ┃ ┗ user
┃ ┃ ┃ ┣ _id
┃ ┃ ┃ ┃ ┗ delete.ts // -> DELETE /management/user/:id
┃ ┃ ┃ ┗ post.ts // -> POST /management/user
┃ ┗ user
┃ ┃ ┣ _id
┃ ┃ ┃ ┗ get.ts // -> GET /user/:id
┃ ┃ ┗ get.ts // -> GET /user
┣ plugins
┃ ┗ ...
┗ index.ts
次に、ルートファイルを記述します。autoloadはファイルパスに基づいてルートのプレフィックスを生成するため、ルートファイル内では、そのプレフィックスに対する相対パスを定義するだけで済みます。通常、index.tsやHTTPメソッドと同名のファイル(get.ts, post.tsなど)の場合、パスは空 '' のままで構いません。
// src/routes/management/user/post.ts
import { FastifyInstance } from 'fastify';
// デフォルトエクスポートを使用し、関数は async である必要がある
export default async function (fastify: FastifyInstance) {
// ここでのパスは空文字列。autoloadが自動的に /management/user プレフィックスを付与する
fastify.post('', async (_request, reply) => {
// 実際のビジネスロジック
return reply.status(201).send({ message: 'User created' });
});
}
最後に、メインのエントリーファイルindex.tsでautoloadを設定します。
// src/index.ts
import autoLoad from '@fastify/autoload';
import fastify from 'fastify';
import { join } from 'path';
const server = fastify({ logger: true });
// ルートの自動ロードを登録
server.register(autoLoad, {
dir: join(__dirname, 'routes'), // ルートが存在するディレクトリを指定
routeParams: true,
dirNameRoutePrefix: true,
});
server.listen({ port: 3000 });
autoLoadの主要オプション解説
dir: 必須オプション。ルートまたはプラグインが存在するルートディレクトリを指します。routeParams: true: 動的ルートを実現するための鍵です。有効にすると、autoloadはアンダースコア_が接頭辞のファイルやディレクトリ名(例:_id)をFastifyのパスパラメータ(例:/:id)に変換します。dirNameRoutePrefix: true: autoloadにディレクトリ構造を使ってルートプレフィックスを生成するよう指示します。これを無効にすると、routes/management/user/post.tsは/management/user/postではなく、単に/postとして登録されます。
2. プラグインの自動ロード
ルート以外にも、autoloadはカスタムプラグインのロードも可能です。プラグインは、再利用可能なロジック(データベース接続、認証、デコレータなど)をカプセル化するための優れた方法です。プラグイン間の依存関係とカプセル化をより良く管理するために、通常はfastify-pluginを使用します。
まず、依存関係をインストールします。
npm i fastify-plugin
次に、Fastifyインスタンスに設定オブジェクトを「デコレート」するプラグインを作成してみましょう。
// src/plugins/config.plugin.ts
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
// プラグインをfp()でラップすることで、Fastifyのカプセル化機構によるプラグインの分離を防ぐ
// これにより、このプラグインでデコレート(decorate)された内容は、外部のすべての子コンテキストから可視になる
const configPlugin: FastifyPluginAsync = async (fastify, opts) => {
const config = {
dbUrl: process.env.DB_URL || 'default-db-url',
apiKey: process.env.API_KEY,
};
// decorateを使用して、設定オブジェクトをFastifyインスタンスに添付する
fastify.decorate('config', config);
};
export default fp(configPlugin);
すべてのプラグインを専用のpluginsディレクトリに配置することができます。そして、再度autoloadを使用してそれらをロードします。
// index.tsに追記
// ...
// プラグインの自動ロードを登録
server.register(autoLoad, {
dir: join(__dirname, 'plugins'),
// matchFilterを使用すると、どのファイルをロードするかをより細かく制御できる
matchFilter: (path) => path.includes('plugin'),
});
// ルートを登録(注意:プラグインはルートより先に登録すべき)
server.register(autoLoad, {
dir: join(__dirname, 'routes'),
// ...
});
matchFilterオプションは非常に便利で、ファイルパスを受け取って真偽値を返す関数を指定できます。これにより、ファイル名や拡張子、その他のルールに基づいてファイルをロードするかどうかを決定できます。
まとめ
@fastify/autoloadは、シンプルに見えて非常に強力なツールです。「設定より規約」という思想を取り入れることで、以下のことを実現できます。
- 定型コードの排除:各ルートやプラグインを手動でimportしregisterする必要がなくなります。
- プロジェクト構造の最適化:ファイルシステムのディレクトリ構造を通じて、API構造を直感的に反映させます。
- 開発効率の向上:開発者はビジネスロジックの実装に集中でき、残りは自動化ツールに任せることができます。
成長を続けるプロジェクトにおいて、早い段階でautoLoadを導入することは賢明な投資です。これにより、あなたのFastifyアプリケーションは初期段階から高い組織性と保守性を維持することができます。