Honoxでhono/clientのパス指定を力技でダイナミックにする

HonoのRPCモードを使用すると、作成したREST APIをSDKライクにアプリケーション側で利用できるが、APIのホスト指定が固定される課題がある。この問題を解決するために、middlewareを使用してクライアントを動的にする試みが行われた。ただし、dummyのURLを指定するとエラーが出るため、ステージング環境やlocalhostなど実際の環境に合わせた設定が必要。クライアント側ではuseEffectを使用した処理は正常に動作するが、本番環境での利用には慎重さが必要。

広告ここから
広告ここまで

目次

    HonoのRPCモードを使うことで、実装したREST APIをアプリケーション側でもSDKライクに使うことができます。ただし現状APIのホストなどを指定する必要があるため、ここをダイナミックにできないか挑戦してみました。

    Hono RPCモードでは、作成したAPIを型安全に扱える

    HonoでREST APIを作成した際、hc()関数でclientをエクスポートすると、クライアント側でclient変数がそのAPIのSDKオブジェクトとして利用できます。

    const app = new Hono()
    .get('/', async c => {
        console.log("called")
        return c.json({
            message: 'hello rpc'
        })
    })
    .get('/rpc', async c => {
        return c.json({
            message: 'rpc'
        })
    })
    
    export default app
    export let client = hc<typeof app>('http://localhost:5173/api')

    パスとメソッドをメソッドチェーンで呼び出すイメージです。

    const data = await client.rpc.$get().then(data => data.json()).catch(console.log)

    ただしhc関数を実行する際に、呼び出すAPIのドメインなどを指定する必要があります。これをデプロイ環境ごとに変わるようにしてみましょう。

    middlewareをつかってクライアントを動的にする

    今回はmiddlewareを使ってみました。app/routes/api/index.tsでclientもexportする際にletで宣言しておきます。その後、middlewareを使って作り直すようにしてみました。

    export let client = hc<typeof app>('dummy')
    
    const hcMiddleware = createMiddleware(async (c, next) => {
        const host = c.req.header('host')
        const protocol = c.req.header('x-forwarded-proto') ?? host?.startsWith('localhost') ? 'http' : 'https'
        const apiBase = `${protocol}://${host}/api`
        console.log([apiBase])
        client = hc<typeof app>(apiBase)
        await next()
    })
    
    const app = new Hono()
    .use(hcMiddleware)

    dummyのまま実行されると壊れそうなので、ここはステージング環境なりlocalhostなりを指定するのが現実的かもしれません。

    middleware方式をつかうなら、Client側のみで?

    上での懸念の通り、サーバー側の処理ではmiddlewareを通す前のhcが利用されました。

    export default async function Test() {
        const data = await client.rpc.$get().then(data => {
            return data.json()
        }).catch(console.log)
        return (
            <pre><code>{JSON.stringify(data,null,2)}</code></pre>
        )
    }

    dummyというダミーのURLを指定していますので、綺麗にエラーが出ます。

    TypeError: Failed to parse URL from dummy/rpc
        at Object.fetch (node:internal/deps/undici/undici:11576:11)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async TestOriginal (/Users/okamotohidetaka/development/my-services/sites/hidetaka.dev.2024/app/islands/test.tsx:25:17)
        at async Promise.all (index 1)
        at async Module.stringBufferToString (/Users/okamotohidetaka/development/my-services/sites/hidetaka.dev.2024/node_modules/hono/dist/utils/html.js:19:26)
        at async Promise.all (index 1)
        at async Module.stringBufferToString (/Users/okamotohidetaka/development/my-services/sites/hidetaka.dev.2024/node_modules/hono/dist/utils/html.js:19:26)
        at async Promise.all (index 1)

    useEffectを使ったクライアント側の処理では動作しました。

    export default async function Test() {
        const [data, setData] = useState('')
        useEffect(() => {
            console.log('1')
            client.rpc.$get().then(data => data.json()).then(data => {
                setTest(data.message)
            }).catch(console.log)
        },[])
        return (
            <pre><code>{JSON.stringify(data,null,2)}</code></pre>
        )
    }

    クライアント側でのみ使う想定にするか、サーバー側処理で利用するパスを固定するか、どちらかでの対応になりそうです。

    いずれにせよ、なかなか力技ではあるので、productionに使うかというとちょっと躊躇いますね・・・

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark