Fastify
Fastifyを触って遊んでる。スピードが速いらしいけど、そういうところは深く考えずに面白そうだから触ってみている。
TypeScript, ESM, PNPM, Biome
TypeScriptとESMで書こうと思って、簡単なプロジェクトを作った。パッケージマネージャにはPNPM、フォーマッター&リンターにはこの前のJSConfで知ったBiome(バイオーム)を使ってみてる。
https://github.com/bufferings/hello-fasitify-ts/tree/8265736961f7fccbbecbd09d29e5136ba81da8dc
Node.jsやPNPMなどのバージョンはこう
❯ node -v v21.2.0 ❯ pnpm -v 8.11.0 ❯ pnpm tsc -v Version 5.3.2 ❯ pnpm biome -V Version: 1.3.3
Biome
今日のメインはBiomeじゃないんだけど、ちょっと触ったのでメモだけ
ちょこっとだけデフォルトの設定から変更を入れている。
.gitignore
ファイルの対象を無視するためにvcs
設定を入れた- フォーマッターのインデントをtab->spaceに変更。行の幅も80->120に変更
- JS/TSのクォートにはシングルクォートを使用するように変更
{ "$schema": "https://biomejs.dev/schemas/1.3.3/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "organizeImports": { "enabled": true }, "linter": { "enabled": true, "rules": { "recommended": true } }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 }, "javascript": { "formatter": { "quoteStyle": "single" } } }
IntelliJプラグインもあるから入れてみてるけど、どういう機能があるのかはいまいちよく分かってない。WebStormのon saveでフォーマットがかかるようにしてみてて便利。
Hello Fastify!
てことで本題。Fastifyのハローワールドはこんな感じ
import Fastify from 'fastify'; const fastify = Fastify({ logger: true, }); fastify.get('/', async (request, reply) => { return 'Hi!'; }); try { await fastify.listen({ port: 3000 }); } catch (err) { fastify.log.error(err); process.exit(1); }
こんな感じで package.json
にスクリプトを入れているので
"scripts": { "build": "tsc", "dev": "node --no-warnings --loader ts-node/esm --watch src/server.ts" },
実行はこう
❯ pnpm dev
叩くとこうなる
❯ curl localhost:3000/ Hi!
--watch
オプションをつけてるのでファイルの内容を変更したらサーバーが再起動されて便利。
Plugins
FastifyにはPluginsという仕組みがある。というか、全部Pluginとして動くって感じなのかな。
このPluginsの仕組みによって、簡単にファイルを分割して管理できる。
プラグインの登録には fastify.register
関数を使う。コールバック形式もあるけど、僕はasync/awaitが好きなので、そっちを使う。さっきのハローワールドのエンドポイントはregister
を使うとこんな感じで書ける。
await fastify.register(async (instance, opts) => { instance.get('/', async (request, reply) => { return 'Hi!'; }); });
Fastifyのインスタンスとオプションをパラメータとして受け取る関数を渡す。ので、この部分を別のファイルに定義してインポートして渡すことができて便利。
インスタンスの階層
register
を使って書き直したハローワールドのエンドポイントは、実際は最初の例とちょっと違う状態になっている。Fastifyのインスタンスは階層化されるのだ。最初の例だと、直接 fastify
インスタンスに get
を登録していたので、ルートインスタンスに定義されていた状態。
register
を使って書き直した方で、そのプラグインに渡された instance
は、元の fastify
インスタンスの子になってる。
階層の確認
FastifyのインスタンスはDecoratorsという仕組みで拡張できる。このDecoratorsを使って、階層を確認してみる。
TypeScriptに型を伝えて
declare module 'fastify' { interface FastifyInstance { top: number; inner: number; } }
トップレベルのインスタンスと、プラグインの中のインスタンスにそれぞれデコレーターを設定してみる。
fastify.decorate('top', 1); fastify.get('/', async (request, reply) => { return { top: fastify.top, inner: fastify.inner }; }); await fastify.register(async (instance, opts) => { instance.decorate('inner', 2); instance.get('/inner', async (request, reply) => { return { top: instance.top, inner: instance.inner }; }); });
結果はこう
❯ curl localhost:3000/ {"top":1} ❯ curl localhost:3000/inner {"top":1,"inner":2}
つまり、親のインスタンスに設定されているものは子から見えるが、子のインスタンスに設定されているものは親からは見えない。
プラグインで書いたものは自分の子のインスタンスに閉じた状態になるので便利。
でも、プラグインから親のインスタンスに手をいれて、他のプラグインから使いたくなることってあるよね。DBのコネクションとか、そういうケースのための仕組みがある。
プラグインから親のインスタンスに設定する
fastify-plugin
パッケージをインストールして、プラグインをそのパッケージの関数で囲むと、親のFastifyインスタンスに設定される。
... import fp from 'fastify-plugin'; ... await fastify.register( // fpでラップする fp(async (instance, opts) => { instance.decorate('innerWithFp', 3); instance.get('/innerWithFp', async (request, reply) => { return { top: instance.top, inner: instance.inner, innerWithFp: instance.innerWithFp }; }); }), );
そうすると親のインスタンスにアサインされるので、親のスコープから見える。
❯ curl localhost:3000/ {"top":1,"innerWithFp":3} ❯ curl localhost:3000/inner {"top":1,"inner":2,"innerWithFp":3} ❯ curl localhost:3000/innerWithFp {"top":1,"innerWithFp":3}
ちなみに、register
に await
をつけておくと、前のプラグインで設定したものが、その次のプラグインから使用可能になる。
面白かった。気が向いたらFastifyのスキーマ定義でも触ってみて遊ぼうかな。
今日の最終的なコードはここ