この記事は frourio チュートリアル連載 第 3 回です。前の記事を読んでいない方は先に読むことをおすすめします!
第 1 回 : frourio でフロントエンドとバックエンドを一緒に静的型検査する - Qiita
第 2 回 : frourio でサクッと API 型定義 & コントローラーを書く - Qiita
第 3 回 : frourio でログイン処理などを行える Hooks を定義する - Qiita
第 4 回 : frourio × aspida で 4 種類のバリデーションを実装する - Qiita
環境
- OS
- Ubuntu 20.04 on Windows 10 Pro (WSL2)
- Ubuntu 20.04 (本物)
- node
v14.14.0
構成
- 基本 Fastify です
Express で大きく違うところは両方書いてあります - Next.js です(今回はあまり関係ありませんが)
- DB などは省略し、最小構成です
概要
frourio には、Fastify 風の Hooks が Fastify / Express ともに再定義されています。
Hooks の種類とライフサイクル
onRequest
preParsing
preValidation
preHandler
これらは、以下のライフサイクルに沿って hook されます。
第 3 引数として受け取れる done
(Fastify) / next
(Express) を呼ぶことで次に進みます。
* async / await 使用時の Fastify を除く(後述)
Incoming Request
│
└─▶ Routing
│
404 ◀─┴─▶ onRequest Hook
│
4**/5** ◀─┴─▶ preParsing Hook
│
4**/5** ◀─┴─▶ Parsing
│
4**/5** ◀─┴─▶ preValidation Hook
│
4**/5** ◀─┴─▶ Validation
│
400 ◀─┴─▶ preHandler Hook
│
4**/5** ◀─┴─▶ User Handler
│
4**/5** ◀─┴─▶ Outgoing Response
出典: Lifecycle of hooks | frourio
最小の Hooks
前回と同様、まずは frourio を dev モードで起動しておきましょう。
hooks.ts
などのファイル作成を検知して、空ファイルの場合にテンプレートを入れてくれます。
yarn dev:frourio
# or
yarn dev # all
ためしに、/api
直下に hooks.ts
を作成してみます。
touch server/api/hooks.ts
すると、以下のような、console.log
するだけの directory level hook
(*後述) が出来上がります。
Fastify の場合
import { defineHooks } from './$relay'
export default defineHooks(() => ({
onRequest: (req, reply, done) => {
console.log('Directory level onRequest hook:', req.url)
done()
}
}))
Express の場合
import { defineHooks } from './$relay'
export default defineHooks(() => ({
onRequest: (req, res, next) => {
console.log('Directory level onRequest hook:', req.path)
next()
}
}))
Hooks の有効範囲
Hooks は、各エンドポイントの controller.ts
または hooks.ts
で export することで有効になります。どちらで export するかによって、Hooks の有効範囲が変わります。
Controller Level Hooks
controller.ts
で export した場合、その階層でのみ Hooks が有効に
(controller level hooks
)
export const hooks = defineHooks(() => ({
preHandler: (req, reply, done) => {
console.log("Hi!")
done()
}
}))
export default
ではないので注意
Directory Level Hooks
hooks.ts
で export した場合、配下のディレクトリ全てで Hooks が有効に
(directory level hooks
)
export default defineHooks((fastify) => ({
preHandler: fastify.auth([
(req, reply, done) => {
...
done()
}
])
}))
async / await を使う
Fastify の場合
export default defineHooks(() => ({
preHandler: async (req) => {
console.log('[/] Directory Level Hook onRequest hook:', req.url)
await new Promise((r) => setTimeout(r, 3000))
console.log('timeout done!')
}
}))
Fastify の場合は本家そのままです。done
を呼ぶ必要はありません。
Express の場合
export default defineHooks(() => ({
preHandler: async (req, _, next) => {
console.log('[/] Directory Level Hook onRequest hook:', req.url)
await new Promise((r) => setTimeout(r, 3000))
console.log('timeout done!')
next()
}
}))
Express では、async / await を使う場合でも、next
を呼ぶ必要があります。
HTTP メソッドによる場合分け
Hooks は、HTTP メソッドを区別せず、コントローラーを定義した全てのメソッドで有効になります。
一部のメソッドでのみ有効にしたい / 一部のメソッドは弾きたい 場合は、第 1 引数 (FastifyRequest
) 内に method
があるのでそれを使います。
export const hooks = defineHooks(() => ({
preHandler: (req, reply, done) => {
if (req.method === 'GET') done() // 早期 done も可能
if (req.method === 'POST') console.log('POST!')
done()
}
}))
同タイミングの Hooks の呼び出し順
複数箇所で同じタイミングでの Hooks が定義されていた場合、以下の順で呼ばれます。
- 先祖から引き継いだ
directory level hooks
- 自身の階層の
directory level hooks
- 自分の
controller level hooks
[/] Directory Level Hook onRequest hook: /api/user
[/user] Directory Level Hook onRequest hook: /api/user
[/user] Controller Level Hook onRequest hook: /api/user
Hooks の実際の呼び出し順は $server.ts
のこのあたりで確認できます。
// (省略)
import hooksFn0 from './api/hooks'
import hooksFn1 from './api/tasks/_taskId@number/hooks'
import hooksFn2 from './api/user/hooks'
// (省略)
export default (fastify: FastifyInstance, options: FrourioOptions = {}) => {
// (省略)
const hooks0 = hooksFn0(fastify)
const hooks1 = hooksFn1(fastify)
const hooks2 = hooksFn2(fastify)
// (省略)
fastify.get(
`${basePath}/user`,
{
onRequest: [hooks0.onRequest, hooks2.onRequest],
preValidation: createValidateHandler(req => [
validateOrReject(Object.assign(new Validators.TokenHeader(), req.headers as any))
]),
preHandler: ctrlHooks0.preHandler
},
methodToHandler(controller4.get)
)
// (省略)
コントローラーの引数を追加する
AdditionalRequest
として request に追加したいものを含めて export し、Hooks 内で req
(第 1 引数) に追加すると、コントローラー内の各メソッドで引数として受け取ることができます。
ちなみに、Hooks の時点では存在が確定していないため、Partial<AdditionalRequest>
が req
に含まれます。
コントローラーの引数には AdditionalRequest
がそのまま含まれます。
* サンプルの /server/api/user/hooks.ts
で型が含まれて無さそうに見えますが、これは fastify.auth([...])
で囲うことによって defineHooks
の型推論から外れてしまうためです。
// request に追加したいものを含めて export
export type AdditionalRequest = {
hoge: string
}
export default defineHooks(() => ({
onRequest: (req, _, done) => {
req.hoge = 'fuga'
done()
}
}))
export default defineController(() => ({
// 各メソッドで hoge を受け取れる!
get: ({ hoge }) => ({ status: 200, hoge })
}))
おわりに
ライフサイクルを理解してつよつよになりたい!
あとこないだ Solufa さんの紹介記事がバズってましたね、この調子で frourio が広まって欲しいです~
憧れのTypeScriptフルスタック環境がコマンド1発で作れる超軽量フレームワーク「frourio」 - Qiita
(私もバズりてぇ~~~)
第 1 回 : frourio でフロントエンドとバックエンドを一緒に静的型検査する - Qiita
第 2 回 : frourio でサクッと API 型定義 & コントローラーを書く - Qiita
第 3 回 : frourio でログイン処理などを行える Hooks を定義する - Qiita
第 4 回 : frourio × aspida で 4 種類のバリデーションを実装する - Qiita