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に使うかというとちょっと躊躇いますね・・・