◆ プラグインの中で追加したフックは そのスコープ内のルートに一致したときだけ実行される
◆ 特定のルートのグループでだけフックを実行したい場合はプラグイン化すると簡単

プラグイン機能を使ったことなかった

Fastify のプラグインを使うとカプセル化できてスコープを作れるというのは知ってはいましたが 基本はライブラリで使う用途だろうと思って 自分でプラグイン機能を使うことはありませんでした

なのでこういう感じで直接ルートコンテキストに register や decorate していました

const fastify = Fastify()

await fastify.register(import("@fastify/static", { root }))

fastify.decorate("foo", "bar")

fastify.addHook("onRequest", async (req, reply) => {
console.log("onRequest")
})

fastify.get("/route", async (req, reply) => {
return { ok: true }
})

await fastify.listen({ port: 3000 })

シンプルなものならこれでも全然困らないですからね
ある程度長くなるとルート定義部分は別モジュールに分けたりはしましたが プラグインという形式は取っていません

スコープが欲しくなった

そんなでしたが ミドルウェアベースで作っていたものを Fastify で実現しようとすると スコープ機能が必要になりました

Fastify はミドルウェアみたいに定義した処理を順番に実行していくものではなく ルートベースの処理になっています
hapi みたいな感じです
ルーティングでルートを決めて そのルートに設定したハンドラの処理が行われます
ルートの処理までに共通処理としてリクエストのパースやバリデーション等があり 自作の共通処理としてフックを追加することもできますが 基本的にはルートの処理がメインです
ミドルウェアのルーターのように ルートの処理をスキップして次に一致するルートの処理をするということもできません

@fastify/static で静的ファイルをサーブするときも内部的には fastify.get や fastify.head でルートを定義してそこでサーブする処理を行っています
そのため 自分で定義したルートも @fastify/static が定義するサーブ処理のルートも扱いは同じです
どちらもルートの処理の前に共通の onRequest フックなどが行われます

これで困るのは cookie のパースやユーザーの取得などの処理が必要ないルートにまでも影響してしまうことです
静的ファイルを返すだけなのにリクエストのたびに cookie をパースしたり ユーザーを識別してデータベースから情報を取ってくるのはムダです
中には静的ファイルでさえも権限チェックしたいということはあるので そういうときには良いかもですけど多くの場合では不要です
レスポンスを速く返したいのでこれらはスキップしたいです

フックの処理で req.routeOptions を見るとどのルートか判断できるので

fastify.addHook("onRequest", async (req, reply) => {
if (req.routeOptions.url === "/*") {
return
}

// ここで cookie やユーザー情報の取得など
})

ということができます
ただ @fastify/static が内部で定義するルートの情報が必要ですし 他にも除外したいルートが増えてくることを考えるとあまり良い方法に見えません
フック内部で自力で分岐するのではなく 定義時にフックが有効なルートを特定の範囲に限定したいです
そう考えていると そういえばスコープ作る機能があったなと思い出しました
使ったことがなかったのでこういうケースでうまく動くのか不安もありましたが とりあえず試してみることにしました

プラグイン機能を試してみる

まずはプラグイン化する前の基本形です
実際に cookie をパースしたりデータベースに接続したりすると長くなるので代わりに onRequest と onSend に console.log を配置しました
この例では 自身で定義した 「/」 へのアクセスでも @fastify/static が定義するルートの適当な静的ファイル 「/foo.jpg」 へのアクセスでも console.log が表示されます

import { fileURLToPath } from "node:url"
import path from "node:path"
import Fastify from "fastify"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const fastify = Fastify()

await fastify.register(import("@fastify/static"), { root: path.join(__dirname, "public") })

fastify.addHook("onRequest", async () => {
console.log("Req")
})
fastify.addHook("onSend", async () => {
console.log("Send")
})

fastify.get("/", async (req, reply) => {
return { ok: true }
})

await fastify.listen({ port: 3000 })

フックを有効にしたい範囲をプラグイン化します

import { fileURLToPath } from "node:url"
import path from "node:path"
import Fastify from "fastify"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const fastify = Fastify()

await fastify.register(import("@fastify/static"), { root: path.join(__dirname, "public") })

await fastify.register(async (fastify, options) => {
fastify.addHook("onRequest", async () => {
console.log("Req")
})
fastify.addHook("onSend", async () => {
console.log("Send")
})

fastify.get("/", async (req, reply) => {
return { ok: true }
})
})

await fastify.listen({ port: 3000 })

こうすることで 「/」 へのアクセスではフックが実行されますが 「/foo.jpg」 へのアクセスではフックが実行されなくなりました
これは便利ですね
ライブラリ以外の自分のコードだけでも register は使う価値があります

ただ この動きを見るとフックを定義してもそのスコープもしくは子プラグインでルートを定義しなければ実行されることはないものになります
この点は注意が必要そうですね

静的ファイルの場合

今回例に使った静的ファイルのサーブですが これに限れば Node.js のサーバーで Fastify でサーブするよりも前段に Nginx などを置いてそっちでやったほうがいいかもしれません

特に今回書いた方法では優先順位がルート優先です
一致するルートがあればそっちが優先されます
一致するルートがない場合に ワイルドカードで定義された静的ファイルのサーブのルートに一致という流れになっています

しかし実際にはその逆で静的ファイルのサーブを優先して ファイルがなければルートの処理としたい場合が多いと思います
それにこっちのほうが一般的だと思います
言語問わずに前段に Nginx を置く場合は自然とそうなります
Apache + PHP でリライトして index.php を使う場合も 静的ファイルが優先です

Fastify でこれをやろうとすると @fastify/static で wildcard を無効にするか @fastify/static を使わず onRequest などのフックで自分でサーブすることになります

wildcard オプションを無効にすると実際に存在するファイルそれぞれをルートとして登録します
URL とルートが完全一致になるのでルーティングの際に静的ファイルが優先されます
しかし 後からファイルを追加してもルートは追加されないのでファイルの追加をサーバーの停止なしにできないです
さらに全ファイルのルートを定義するので node_modules みたいな膨大なファイル数のフォルダをサーブする場合はかなり重たくなりそうです

onRequest の方法は Nginx での前段処理に近い感じですし ミドルウェアの方法にも近い感じです
フックなのでルーティングに依存せず 各ルートの処理より前にファイルが見つかればレスポンスを返してしまいます
見つからなければ何もしないので本来のルートの処理が行われます
自分でサーブする場合は @fastify/static が内部で使っている @fastify/send を使うと楽です
ちなみに hapi を使ったときもこの問題があって この方法で対処していました (hapi には Fastify の hook に相当する extension 機能があります)