🙄

【Honoミドルウェア】多分この書き方を知ってたら救われる編

2025/01/20に公開

記事を書いた経緯

Honoのmiddlewareの公式ドキュメントの情報って全然豊富じゃないですよね?
Honoのコンセプトは大好きなのにミドルウェアの痒いところに手が届かずにLaravel(かRails)に帰って行った個人開発プログラマーも多いはず。。。
そこで、ミドルウェアの「多分この書き方を覚えておけばいいですよ」というよう使い方を紹介します。

コンテンツ

  • createMiddlewareを使ったオリジナルミドルウェアの定義方法
  • exceptを使った特定ルートのミドルウェアの除外
  • everyを使ったミドルウェアの連続適用(チェーン)

createMiddlewareを使ったオリジナルミドルウェアの定義方法

トークンを取得して期限切れだったら401を返してアクセスをブロックするコードを例に説明します。
createMiddlewareで自己定義のミドルウェアを作成しています。

import { createMiddleware } from "hono/factory";
import { HonoEnv } from "..";

export const checkIfTokenValid = createMiddleware<HonoEnv>(async (c, next) => {
    const tokenRepository = c.get("tokenRepository")
    const token = c.req.header("Authorization")?.split(" ")[1].trim()
    // なんでもいいのでトークンを取得する
    const tokenRecord = await tokenRepository.fetch({
        token: token!
    })
    // トークンが有効期限切れだとブロックする
    if(tokenRecord.expirationDate.getTime() < new Date().getTime()) {
        return c.json({
            "message": "UnAuthorized (Your token is out of date.)"
        }, 401)
    }
    await next()
});

exceptを使った特定ルートのミドルウェアの除外

exceptは特定のルートに対してミドルウェアの適用をしないでおくことができる関数です。
公式ドキュメントにはこんな感じで書いています。

// If client is accessing public API, skip authentication.
// Otherwise, require a valid token.
app.use('/api/*', except('/api/public/*', bearerAuth({ token })))

「特定のルートだけミドルウェアを適用しないことができる!!!すごい!!!!」
と思ったんですけど、複雑な条件でのミドルウェアの除外の要件が出てきた途端に全く使い物にならなくなりました。
どうにかできないかと思って、型の定義を見ていたらどうやらこういう書き方ができるみたいです。

app.use(
	"/api/user/*",
	except((c) => {
		if (
			c.req.method == "OPTIONS" ||
			c.req.path.includes("login") ||
			c.req.path.includes("register") ||
		) {
			return true;
		} else {
			return false
		}
	})
.....

exceptはルートが書かれた文字列意外にもcontextを引数にとる関数を渡すこともできます
関数の中で特定の条件を含む条件文を書いて、返り値がtrueの時はミドルウェアの適用が除外されます。

everyを使ったミドルウェアの連続適用(チェーン)

Laravel経験者がHonoを扱い初めてcreateMiddlewareを使用してミドルウェアを作り始めると、自作ミドルウェアを連続で適用するコードを書くのがめんどくさすぎてLaravelに帰っていきますが、"every"メソッドを覚えておくと、ちょっとLaravelに帰りたくなる程度で済みます。

	every(
			// ここにミドルウェアのロジックを追加
			const jwtMiddleware = jwt({
				secret: c.env.AUTH_SECRET,
			});
			// Run JWT middleware first
			await jwtMiddleware(c, next);
		},
		checkIfTokenValid,
		checkIfUserRole
	)

上のコードはHonoのビルドイン機能であるJWTによる認証を行った後にトークンの有効性を確認して
最後にトークンの属性がユーザーであることを確認しています。
everyを使ってカンマ区切りでミドルウェアを書いていくことで連続的に簡単に適用ができます。createMiddlewareで作った関数には"(c, next) => ...."のコールバック関数は渡さなくて大丈夫です。"every"関数が勝手に渡してくれます。
ビルドインのJWTミドルウェアに関してはsecretを渡してインスタンス化(であってる?)しないといけないので自力でこねくり回します。

最終的なコード

app.use(
	"/api/user/*",
	except((c) => {
        // へたっぴな条件分岐で特定のルートを除外
		if (
			c.req.method == "OPTIONS" ||
			c.req.path.includes("login") ||
			c.req.path.includes("register") ||
		) {
			return true;
		} else {
			return false
		}
	},
    // everyを使って順番にミドルウェアを適用する
    every(
		async (c, next) => {
			// ここはビルドインのJWTミドルウェアなのでシークレットを渡す
			const jwtMiddleware = jwt({
				secret: c.env.AUTH_SECRET,
			});
			// Run JWT middleware first
			await jwtMiddleware(c, next);
		},
		checkIfTokenValid,
		checkIfUserRole
	)),
);

終わりに

何か間違えていたら優しく教えてください。

Discussion