Hono練習

ここで作ってく

データベースとORM系どっちも知識皆無なので、全部担ってくれるSupabaseに頼る
honoのapi書くファイル内でsupabaseのjsコード書いてdbアクセスして、て感じでいけそう
apiエンドポイントがあるとは言えさすがにクライアントルーティングは別で用意する必要がありそうなのでtanstack routerも使うことになると思う

ディレクトリ構成はこうなりそう
project
├──front
│ └──src // React
└──back
└──index.tsx // Hono & Supabase
frontでcreate vite
、backでcreate hono
するか

- フロントエンド
npm create vite@latest frontend
- バックエンド
npm create hono@latest backend
honoのテンプレートはcf pagesで良いか(よくわからんが)
PagesとWorkersは機能的にも似通っていてややこしいのですが、ざっくりとPagesは静的コンテンツ向けのホスティングサービスで、シンプルなデプロイや配信に最適で、Workersは動的な処理やサーバーレスアプリに特化した、より高度なカスタマイズが可能なサービスです。
バックエンド側でsupabaseインストール
cd backend/
npm i @supabase/supabase-js

次、TanStack Router導入/セットアップ
npm install @tanstack/react-router
npm install -D @tanstack/router-plugin
@tanstack/router-devtoolsは前使わなかったので良いや
vite.config.tsのimport react from '@vitejs/plugin-react'
は見つからなかったのでimport react from '@vitejs/plugin-react-swc'
に変更

メモ
- Honoのインスタンス作る時に使用するBindingsって型はなんだ
- middlewareっていうのが何なのかわかってない
- 認証とか?でもtanstackのbeforeLoadでsupabase-js書けば認証できちゃうな
- jsxをhonoから返せるっぽい(今回それやるかはわからん)

middlewareは、tanstack routerのbeforeLoadみたいなクライアントルーティングでのpathアクセス前に認証かけたりするやつとは違って、apiエンドポイントアクセス時にそのエンドポイントにおいて何かをreturnする前に認証かけたりするやつっぽい。CORSをhono middlewareで書いたらイメージ掴めた

さて、HonoでAPI書いていく
frontを5000, backを5001ポートで常時立てておく
なのでフロントからhttp://localhost:5001/
のhono apiエンドポイントにfetchする感じかな

http://localhost:5001/api
に変更

c.renderしちゃうとjsxがそのままレンダーされて画面表示されちゃう気がするので、ただレスポンスしてほしいな
レスポンスはどうやってやるんだ?
geminiに聞いた。return c.text
でいけるのか
app.get('/', (c) => {
return c.text('はじめる')
})

んでそれを受け取るフロント側も初めて書くがやってみよう
- まずfetchで得る型は
Promise<Response>
ぽい - それをresponseとして受け取ったら、更に
response.text()
までしてやっと生のテキストとして使える
text() は Response インターフェイスのメソッドで、 Response ストリームを取得して完全に読み込みます。文字列で解決するプロミスを返します。
今回honoからはreturn c.text()
でテキストとして返してるから、jsonとして解釈しようとするrespose.json()
は使えないのでresponse.text()
使うってことか
json() は Response インターフェイスのメソッドで、 Response のストリームを取得して完全に読み取ります。本体のテキストを JSON として解釈した結果で解決するプロミスを返します。
なお、このメソッドは json() という名前であるにもかかわらず、結果は JSON ではありません。入力として JSON を取って解釈し、 JavaScript のオブジェクトを生成します。

フロント側でCORSエラーが出たのでHono側でCORSコードを書く必要もありそうだ
用意されてる、見つけた
originをhttp://localhost:5000/
にしてあげるとかでいけそう
あとTanStack Queryとか使って、use()
に渡すpromiseをキャッシュしろというのをよく聞くのでその導入もやりたい
キャッシュしないとレンダリングごとに毎回fetchして新しくpromise作っちゃうから非効率とかそういう理由だと思う(そんでそこから下手すると無限レンダリングに繋がったりするのかも?)

よし、Honoで以下ミドルウェア書いたら画面描画が理想通り機能した
app.use(
cors({
origin: 'http://localhost:5000',
})
)
だがコンソールにエラーは出ている
まさにこれだな
キャッシュしてないpromiseが見つかったら自動でエラー出して「キャッシュしろ!」て警告を出すReactの仕様だろう
同じくuse()
使ってSupabaseのAPI叩いたときは何もエラー出なかった理由は、supabaseのDB APIにはすでに全origin許可のCORS設定が書かれているのだろう
あーやっぱそうっぽい、これだ。なるほど

とりあえずTanStack Queryを導入してキャッシュしていく
TanStack Query編
npm i @tanstack/react-query
npm i @tanstack/react-query-devtools
supabaseコードもawait使うような非同期処理なのでbackend/内にもインストールすべきか?
いやでも...
Suspense機能させたい
↓
use()
使うしかない
↓
tanstack query導入してキャッシュせねば
という流れなので、クライアント側でSuspenseしちゃえばバックエンドの非同期待ちは拾えるから普通にSuspense機能させることができるのでバックエンドではtanstack query不要か
まぁでもbackend/ではキャッシュできなくなるからちょっと非効率にはなるのかも
なので余裕があったらbackend/でも導入しよう
とりあえずfrontend/で導入していく

セットアップはこちらも参考にできそう(react-queryに関しても書かれてるので)
公式

まずこれ読んでReactQueryDevtools書く
んでこれ読んでQueryClientProviderとかそのあたり書く
既にtanstack routerのRouterProviderもあるのでその同階層にQueryClientProviderを書けば良いのか親に書けば良いのかわからないが以下を参考にすると親で良さそう
公式にも同じように書かれてたのでやはり親で良さそう(なぜqueryの方が親になるのかは気になるが)
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>,

useSuspenseQueryというのがあるっぽいがこれ使えばuse()
使わずに普通にawait使ってfetchしてもSuspense機能しそう...!!!?めっちゃ助かるぞ
無限レンダリング回避するためuse()
使うコンポーネントとfetch()
使うコンポーネント分割しないといけなくていろいろ面倒だったがそれが不要になるのか
いやtanstack query導入したらその分割も不要になる気はするからそれは関係ないかも...
まぁuse()
使わずにfetchだけで良くなるのはどっちみち助かる
そしてuseQueryとほぼ同じ使い方っぽい、素晴らしい
注意点もあるっぽいので一応メモ

useQuery(useSuspenseQuery)では関係ないが、useMutation内でinvalidateQueriesしたい時はqueryClient.invalidateQueries
とやる必要があるのでqueryClientというのが必要になってくる
んでそれはuseQueryClientというのを使えば取得できる
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()

ディレクトリ設計どうすべきだろうか
ん-これ通りservice/というのを作るとtanstack routerで既にパスとして分離できてるディレクトリと被る気がするなぁ
こっち参考にして、普通に-functions/っていうの作ってそこに全部突っ込むか

とりあえずここ以降を参考にして同じように実装してみる
できた!!use()
使わずにawaitしてるだけなのにSuspense機能してる!useSuspenseQuery凄い!
エラーも消えてる
だが1つ気になる点がある
tanstack queryでisPendingの状態を取得する必要があるのかどうかわからない
tanstack routerの方でpendingComponent:
で設定しているので絶対重複してるんだよな
どうすれば良いだろう、どっちを使えば良いんだろう

試してみたら、isPendingの方は完全に機能していないことがわかった
tanstack routerのpendingComponentしか機能してない
pendingComponentを消してisPendingのコンポーネントだけ表示させようとしたらそもそもpending中は画面真っ白になってSuspense機能してなかった
更に....useSuspenseQueryではなくuseQueryにしてみたのだが、それでもなぜか普通にSuspense機能してる
なぜ??気になるがとりあえず飛ばす。pendingComponentを書けばokということをとりあえず暗記する。そして一応useSuspenseQueryの方を使っていくことにする

違いはdataの状態だけであって、どっちもSuspenseに対応できているという点は同じなのかもしれない(?)
通常のuseQueryとuseSuspenseQueryの大きな違いはdataがundefinedな状態を考慮する必要がなくなったことです。
だからuseSuspenseQuery使うならisPendingもisErrorも返り値として受け取る必要は無くなってdataだけ受け取れば良いっぽい。アツい

とはいえもはやReactの<Suspense></Suspense>をまったく使用していないのにSuspenseが効いてるのがなぜかわからず混乱してるのだが、そこから考えられることは...
pendingComponentが全部やってくれてる。最強。
ということ。
だがまぁそれだけという訳でもなく、useQuery/useSuspenseQueryを使っていることが前提という部分も大事か。
なぜなら普通にコンポーネント内でawaitするのは当然不可。クライアントコンポーネントはawait禁止なので
んでごり押しでuseEffect内でasync関数作ってその中でawaitしてそれを実行しようとしてもそれはSuspense機能しない。
サスペンスはエフェクトやイベントハンドラ内でデータフェッチが行われた場合にはそれを検出しません。
なのでtanstack queryみたいに隠蔽してくれるやつ使わないと無理だよね、という話はある
だがそれさえ使っちゃえばもう<Suspense>使わずとも勝手にSuspense機能してpendingComponentがfallbackとして表示されるようになる
それでいいのか?<Suspense>の存在意義めっちゃ薄くない?という混乱はあるが、とりあえずめっちゃ楽に書けるという事実は変わらないので助かる
ん-、まぁReactが将来を見越しての話なのかなー?という予想
tanstackとかのライブラリを全く使わずReact単体でもuse()
と<Suspense>だけ使えば非同期やらキャッシュやらを完全に対応できるようになるかもしれないという未来があり、そのReactオンリーという栄光のために残してある、ということなのかもしれない(?)

なんかこのpreloadとかもやりたいなぁというメモ
tanstack routerのloaderとuseSuspenseQueryを組み合わせるというもの
そうすれば既にuseSuspenseQuery発火の段階ではデータが揃ってるらしい、凄い

とりあえずtanstack query実装完了!
現状こんな感じ
TanStack Query編終了

さて、todos/パスとか本格的に作っていきたい
のでsupabaseでtable作ったりsupabase-jsをhonoの中で書いたりしていくか

ていうかjsxをhonoから返す予定は無いのでこのrenderer.tsxっていう規定のファイル不要かも?
import { jsxRenderer } from 'hono/jsx-renderer'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
{/* <link href="/static/style.css" rel="stylesheet" /> */}
</head>
<body>{children}</body>
</html>
)
})
index.tsxからミドルウェアとして呼び出してるっぽいんだけれども
import { renderer } from './renderer'
app.use(renderer)
まぁいいや

Supabase設定編
この辺り見つつ、supabaseのプロジェクト初期化、接続までやっていく
backend/に.env作成
前ハマったやつ。アクセスポリシー作らないとfetchしてもtableから空の配列しか返ってこないので注意
ここで設定できる
checkは無しで消して、usingの中をtrueにすれば良い
textかvarcharか...違いはあんま無さそうなのでとりあえずvarcharで
foreign keyも設定し、updateもdeleteもcascade(滝のように流れる=連動する=一心同体)で設定
todos.id -> users.idにした。userが消えたらtodoも全部消えるというロジックが理想なので、userが主従の主であるべき
毎回ちゃんとemail認証かけるの面倒だし1時間に3通までしか送れないのでとりあえずConfirm emailはオフにしとく
あとはsupabase.auth.signUp()
すればAuthentication -> usersに勝手に登録される

あとはsignUpして、そのユーザーからtodoを何個か登録して、select/create/update/delete確認すればとりあえずokかな
signup
const { data, error } = await supabase.auth.signUp({
email: 'example@email.com',
password: 'example-password',
})
fetch/insert/update/delete

-
getSession
はブラウザのローカルセッションを見るだけでサーバーに確認してる訳じゃないので改ざんされてる可能性を否定できないのでgetUser
の方が良いらしい
Since the unencoded session data is retrieved from the local storage medium, do not rely on it as a source of trusted data on the server. It could be tampered with by the sender. If you need verified, trustworthy user data, call getUser instead.
const { data, error } = await supabase.auth.getSession()
まぁクライアントサイドで高速にチェックしたい場合はgetSession
使えって感じらしいけど
On the client, you can instead use
getSession().session.user
for faster results.getSession
is insecure on the server.
-
getUser
はこれ。こっちはちゃんとサーバーに問い合わせてチェックする
const { data: { user } } = await supabase.auth.getUser()
ということでgetUser
使うか

いやでも使い分けすべきかも?
-
getUser()
でアカウント登録つまりsignupが済んでいるのか確認できる -
getSession()
でログイン中つまりsignin中が確認できる- ただ改ざんされる可能性はあるというのが面倒だな(今回は無視するか...)
いや違うな、どっちもセッション確認してログイン中かどうか見てるっぽいのでどっちも役割同じだ
アカウント作成済みかどうかみたいな情報に関してはそもそも不要だし不可能かもしれん
なぜなら、何も入力されてない状態でどうやってアカウント存在確認をするんだという話になるから
メアド打ってくれればそれを基にアカウント作成済みか確認できるが、セキュリティ的にそれは良くなさそうだし、何よりそんな手間をユーザーに求めるなら最初からユーザーに新規登録させてエラーで「既にアカウント登録済みです」って出すのと何ら変わらないのでそれで良くないかという感じがした
なので新規登録ボタンを出すべきかログインボタンを出すべきかというのを自動検出してどっちかだけを画面に出すみたいなことは不可能 & 不要だと判断したのでやらないことにする

envが読み取れない、初見のエラーだ
Uncaught Error: supabaseUrl is required.
-
VITE_
prefixはつけてる-
import.meta.env.○○
で読み取ろうとしてる
-
- .envの階層をbackend/でもルートディレクトリでも試した
- サーバー立てなおした
- .gitignoreに指定するのも指定しないのも試した
Hono使ってるからhono側のenv設定方法使わないといけないとか?

あーやっとわかった
frontend/直下に.env設置しないとだった。Viteが入ってるのはfront/のみなのでそのディレクトリ内に入れないといけないということなのだろう
ルートディレクトリでもbackend/でもダメだがfrontend/ならokというところは盲点だった
知見:Viteがインストールされているpackage.jsonの同階層に.envを置く!

追記
ここの認識がまだ甘かったのでめっちゃ痛い目を見た
んでbackendも同じようにvite入ってるよな・・?とは思う
supabase clientを使いたいファイルと同ディレクトリ内(backend/かfrontend/)の直下(もしくは親ディレクトリの直下でも良い?)に.env置かないとだめだった
なので兄弟ディレクトリ(?)の直下に置いちゃうと読み取れずにinternal server errorになって詰む

getUser
でtrueが取れれば認証は不要にするか
getSission
の方は自動でリフレッシュトークン発行するらしいけどgetUserの方は特に無さそうだが、これはつまり一度新規登録すると一生trueが返ってくるということだろうか
If the session has an expired access token, this method will use the refresh token to get a new session.
セッションに期限切れのアクセストークンがある場合、このメソッドは新しいセッションを取得するためにリフレッシュトークンを使用します。
だとするとややセキュリティ上良くなさそうではあるが(?)、まぁいい

signup, signin, signout のリクエストを実際に送る
- signup
- signin
- signout

ぐわー、またNavbarのコンポーネント内でuseEffect, useState使ってログイン状態をawaitで取得してsessionがあったらログアウトボタンを表示するという長いコードを書いてしまっている
ラグが視覚的に顕著だしコードも見づらいからできればuseEffect書きたくないんだよなぁ、わざわざ中にasync関数を作ってそれを実行するというのも面倒だし
んでuseEffect使うならuseStateもセットで使わざるを得なくなりどんどんコード量が増えていく
とにかくこれ良くない気がする
import { Link, useNavigate } from "@tanstack/react-router"
import { signOut } from "../auth/-functions/signOut"
import { isLoggedIn } from '~/backend/supabase/auth/isLoggedIn'
import { useEffect, useState } from "react"
export default function NavbarTop() {
const [session, setSession] = useState<boolean>(false)
const navigate = useNavigate()
useEffect(() => {
(async() => {
const _session = await isLoggedIn()
if (_session) {
setSession(_session)
}
})()
}, [])
const handleClick = async() => {
const ok = await signOut()
if (ok) {
navigate({ to: '/' })
}
}
return (
<div>
<Link to="/">ホーム</Link>
{session && <button onClick={handleClick}>ログアウト</button>}
</div>
)
}
useContextとか使ってグローバルにログイン状態保持するべきなんだろうけども、
現在のコンポーネント階層の設計上できないんだよなぁ
つまりNavbarの親でログイン状態を取得しておけばuseContextのProviderから子であるNavbarやらその他の子にcontextであるログイン状態を渡せるんだけど、Navbarの親は/
つまりRootであって、そこではログイン状態取得する必要無いので無駄だし遅くなるから避けたい
だがそうなるとNavbarが自分自身でawait isLoggedIn()
して取得するしかない、のでさっきのようなuseEffect & useStateコードを書かねばならない・・
とはいえ/
(Root)のbeforeLoadでawait isLoggedIn()
してその返り値を認証必要なページ全部に渡すような設計にするのが理想っぽい気はしている
なのでそうしたい、のだが...
beforeLoad:
のasync関数でreturnした値をcomponent:
に渡す方法がわからない
前挑戦したがダメだった。これは気力ある時にリベンジしたい
うーーーーーむ・・・とりあえず今回はhonoの勉強なのでここは飛ばす
(まぁ機能自体は実現できてるので問題は無い)

メモ
-
<Outlet />
を移動するなり中身改造するなりしても案外いけるのかもしれない(?)- 現在のコンポーネント階層を紙に図で書いてイメージしやすくすべきか
- 参考になりそうなサイト

ん?なんかsignIn()
は旧式ゆえ使えないっぽいぞ
あーsupabaseのv1のdocument見てたからっぽい、最新はv2なのか
v2だとこれだな。signInWithPassword
か

よしsignup, signin, signoutは実装できた
次はtodos/$userId
という動的path作ってそこで該当ユーザのtodoリストを表示するか
useParams()
でpath parameterを取得できそう

index.tsx内にコンポーネントも一緒に書いちゃうならRoute.useParams()
でいけるんだけど、コンポーネントは別ファイルに分離したいので、そうなるとRouteを参照することができない
だがuseParamsの引数from
を入れれば解決するっぽい
const { userId } = useParams({ from: '/todos/$userId/' })
よし、todos/$userId
でuserIdを取得できてることを確認した

ちなみに何となく"signin"ってわかり辛いよなぁという感があるのでユーザへの表示は"ログイン"にしてる
ユーザに表示されないコード部分はsigninにしてるが(supabaseの用意された関数名もsignIn
だし)

signin / signup 成功後のredirect pathも/todosではなく/todos/<userId>に変更せねば
なのでgetUserした後にuserのUUIDを取得する関数を作る必要あるな
多分getUserの返り値.data.user.id
でいけそうだ

新しい知見を得た
throw redirectする際にテンプレートリテラルは使えないっぽい
つまりこれはエラーになる
throw redirect({ to: `/todos/${userId}` })
エラーメッセージ
Type '`/todos/${any}`' is not assignable to type '"/todos/$userId" | "/" | "." | ".." | "/auth"'.ts(2322)
なのでこうしないといけない
throw redirect({
to: "/todos/$userId",
params: { userId }
})
こうするとエラーが消えた。へぇ~
claude 3.7
このエラーは、TypeScriptが /todos/${userId}
という動的に生成されたパス文字列を、TanStack Router の redirect
関数の to
プロパティに受け入れられる型と一致しないと判断しているためです。
この問題を解決するには、以下のいずれかの方法を試してみてください:
-
redirect
関数に型アサーションを使用する:
import { redirect } from "@tanstack/react-router"
import { getUser } from "~/backend/supabase/auth/getUser"
export const checkSession = async() => {
const user = await getUser()
const userId = user?.id
if (userId) {
throw redirect({
to: `/todos/${userId}` as const
})
}
}
- パスを作成する際にルートパラメータオブジェクトを使用する(推奨):
import { redirect } from "@tanstack/react-router"
import { getUser } from "~/backend/supabase/auth/getUser"
export const checkSession = async() => {
const user = await getUser()
const userId = user?.id
if (userId) {
throw redirect({
to: '/todos/$userId',
params: { userId }
})
}
}
TanStack Router では、パスパラメータを文字列内に直接埋め込むのではなく、params
オブジェクトを通じて渡すことが推奨されています。これにより型安全性が確保され、このようなエラーを防ぐことができます。
エラーメッセージを見る限り、あなたのルート定義では /todos/$userId
というパスが定義されていて、文字列テンプレートリテラルではなく、このような形式で指定する必要があります。

よし動的path(todos/$userId
)は対応できた
いよいよhono内にsupabase-jsで実際にORM的なコードを書いていくか
todos/$userId
から/api/todos/$userId
的なところ(?)にリクエスト送って該当ユーザのtodosを返却してもらってそれを表示したい
んで追加/削除/編集にも対応したい

今mockで/api
にリクエスト送ってそれを受け取るというのをtanstack query使って実装できてるのでそれを参考に書いていく
honoのディレクトリ構成もちゃんとやりたい
あとsupabaseのディレクトリは自分なりに考えて作っていくか

supabase-jsやreactのfetch方法を聞いたメモ
claude 3.7
reactのフロントエンドからhonoのAPIを通じてsupabaseからユーザー固有のTODOを取得するシステムを実装しましょう。以下に各部分のコード例を示します。
Honoでのバックエンドコード
import { Hono } from 'hono';
import { createClient } from '@supabase/supabase-js';
// Supabaseクライアントの初期化
const supabaseUrl = process.env.SUPABASE_URL || '';
const supabaseKey = process.env.SUPABASE_ANON_KEY || '';
const supabase = createClient(supabaseUrl, supabaseKey);
// HonoアプリのセットアップとCORSの設定
const app = new Hono();
// TODOを取得するエンドポイント
app.post('/api/todos', async (c) => {
try {
// リクエストボディからuserIdを取得
const { userId } = await c.req.json();
if (!userId) {
return c.json({ error: 'userId is required' }, 400);
}
// Supabaseからユーザー固有のTODOを取得
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', userId);
if (error) {
console.error('Error fetching todos:', error);
return c.json({ error: error.message }, 500);
}
return c.json({ todos: data });
} catch (error) {
console.error('Unexpected error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 新しいTODOを作成するエンドポイント
app.post('/api/todos/create', async (c) => {
try {
const { title, userId } = await c.req.json();
if (!title || !userId) {
return c.json({ error: 'title and userId are required' }, 400);
}
const { data, error } = await supabase
.from('todos')
.insert([{ title, user_id: userId, completed: false }])
.select();
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({ todo: data[0] });
} catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
});
export default app;
Reactでのフロントエンドコード
import { useState, useEffect } from 'react';
interface Todo {
id: number;
title: string;
completed: boolean;
user_id: string;
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodoTitle, setNewTodoTitle] = useState('');
const [loading, setLoading] = useState(false);
// 実際のアプリケーションでは認証システムからユーザーIDを取得
const userId = 'user123'; // 例としてハードコード
// TODOを取得する関数
const fetchTodos = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:3000/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
});
if (!response.ok) {
throw new Error('Failed to fetch todos');
}
const data = await response.json();
setTodos(data.todos);
} catch (error) {
console.error('Error fetching todos:', error);
} finally {
setLoading(false);
}
};
// 新しいTODOを作成する関数
const createTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTodoTitle.trim()) return;
try {
const response = await fetch('http://localhost:3000/api/todos/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newTodoTitle,
userId,
}),
});
if (!response.ok) {
throw new Error('Failed to create todo');
}
const { todo } = await response.json();
setTodos([...todos, todo]);
setNewTodoTitle('');
} catch (error) {
console.error('Error creating todo:', error);
}
};
// コンポーネントがマウントされたらTODOを取得
useEffect(() => {
fetchTodos();
}, []);
return (
<div className="todo-app">
<h1>TODOリスト</h1>
<form onSubmit={createTodo}>
<input
type="text"
value={newTodoTitle}
onChange={(e) => setNewTodoTitle(e.target.value)}
placeholder="新しいTODOを入力"
/>
<button type="submit">追加</button>
</form>
{loading ? (
<p>読み込み中...</p>
) : (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} - {todo.completed ? '完了' : '未完了'}
</li>
))}
</ul>
)}
</div>
);
}
export default TodoApp;
Supabaseのテーブル構造
Supabaseでは、以下のようなテーブル構造を想定しています:
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
user_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ユーザーIDにインデックスを追加してクエリを高速化
CREATE INDEX todos_user_id_idx ON todos (user_id);
実装のポイント
-
Honoのバックエンド:
- POSTエンドポイントを使用してuserIdを受け取ります
- Supabase-jsを使って、そのuserIdに関連するTODOを取得します
- エラーハンドリングと適切なHTTPステータスコードを返します
-
Reactのフロントエンド:
- TODOデータのfetchと状態管理
- userIdをリクエストボディに含めてAPIにPOSTリクエスト
- 新しいTODOの作成機能も含んでいます
実際の実装では、認証システムと連携させてユーザーIDを動的に取得する必要があります。また、環境変数の設定や適切なCORS設定も忘れないようにしてください。
何か具体的な質問や機能追加があれば、お知らせください。

requestの受け取りはc.req.XXX
でいろいろできそうだ
今回はidをpath paramsにしているのでc.req.param
を使う感じかな
app.get('/api/todos/:id', async (c) => {
const id = c.req.param('id')
})
どうせ中でsupabase-jsをawaitで実行することになるからasyncは必須になるだろう

次にreactからのリクエスト方法を調べる
todos/$userId
のbeforeLoadでuserId取得してそれをリクエストとして送りたい
fetchのオプションでmethod, headers, bodyを設定したりするのかな
んで今回はそれをtanstack query使ってやる必要あるから難しくなりそうだ

beforeLoadでの返り値(userId)をcomponentに渡すことはできるのかという問題、普通にpropsとして自動で受け取れるようになってる...!?もしそうならめっちゃ助かるぞ
claude 3.7
はい、beforeLoad
の返り値を component
で受け取ることができます。これはTanStack Routerの機能の一つで、ルート間でデータを共有するのに便利な方法です。
beforeLoad
関数は、コンポーネントがレンダリングされる前に実行され、その返り値はルートのコンポーネントにプロップとして自動的に渡されます。例えば:
// checkSession.ts
export async function checkSession() {
// セッションチェックのロジック
const userData = await fetchUserData();
return { user: userData };
}
// Auth.tsx
function Auth(props) {
// props.user にアクセスできる (beforeLoadからの返り値)
const { user } = props;
return (
<div>
<h1>ようこそ、{user.name}さん</h1>
{/* その他のコンポーネントの内容 */}
</div>
);
}
あなたのコード例では、checkSession
関数の返り値は Auth
コンポーネントで利用できます。また、pendingComponent
として指定した AuthPending
コンポーネントは beforeLoad
の処理中に表示され、エラーが発生した場合は errorComponent
として指定した AuthError
コンポーネントが表示されます。
この仕組みを使って、認証状態やユーザーデータなどをルートロード時に取得し、それをコンポーネントに直接提供することができます。
試してみる

というかtodos/$userId
のpathでbeforeLoadするとき、userId自体はpathからそのまま取得できそうだな
んでそのuserIdを基にtodosデータ取得して、それを返却してcomponentにpropsとして渡す、という流れが妥当か

fetchこれ

ん、これもしかして、TanStack Queryでfetchをキャッシュするという行為が不要になるか?
Supabaseのクエリをsupabase-jsで書くわけだがこれは別にfetchでもないしなぁ
まぁでもPromiseを返すという点では同じ。どうすべきだろうか

ん?なんかいろいろミスってた
react側で全部supabase-jsのクエリ書いたりとかやろうとしてた、honoでクエリは実行せねば
流れ
react側のtanstack queryでhonoの/api/todos/:userId
をfetch
↓
honoでsupabase-jsのクエリ実行してその結果としてのtodosを返却
↓
それをreactが受け取り表示

なぜかCORSエラーが出だした。origin許可してるのに。なぜ??
(しかもずっといけてたのに急にエラーになった)

なんかfetch先のurlを相対パスにするか絶対パスにするかでエラーメッセージ変わるんだが、どっちでも良いんじゃなかったっけ?
const res = await fetch(`/api/todos/${userId}`) // これだとJSONではなくHTMLが返ってくるエラー
const res = await fetch(`http://localhost:5001/api/todos/${userId}`) // これだとCORSエラー
多分前者はエラー発生してサーバのデフォルトのindex.htmlのhtml返しちゃってるみたいなことだと思うので完全にfetchが機能してないような気はする
だから後者の絶対パスの方が正しくて、ただそこでCORS設定してないからエラーになっちゃってるだけ、て感じではないだろうか(?)
あーやっぱそうっぽい。初歩的なミスだが、ポートが変わるなら絶対パスにしないといけないらしい。相対パスにしないと同じポートを想定した上でリクエスト送っちゃうのか
gemini-2.0-pro-exp-02-05
fetch
の引数の URL は、絶対パスでも相対パスでも問題ありません。どちらを使用するかは、状況とコードの構成によって異なります。
絶対パス:
- 常に
http://
やhttps://
から始まる完全な URL です。 - どの場所から呼び出しても、常に同じリソースを指します。
- 異なるドメインやポート番号のリソースにアクセスする場合に必要です。
相対パス:
-
fetch
を呼び出す JavaScript ファイルの場所を基準とした URL です。 - 同じドメイン内の異なるパスにアクセスする場合に便利です。
-
./
(現在のディレクトリ)、../
(1つ上のディレクトリ) などを使用してパスを指定します。
http://localhost:5000/todos/$userId
から http://localhost:5001/api/todos/:userId
に fetch
する場合:
この場合は、異なるポート番号 (5000 と 5001) にアクセスするため、絶対パスを使用する必要があります。相対パスでは、ポート番号を変更することができません。
コード例 (絶対パス):
const userId = 123; // 例
fetch(`http://localhost:5001/api/todos/${userId}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
補足:
フロントエンドが http://localhost:5000
で動作している時、fetch('/api/todos/123')
のようなバックエンドのapiのルートからの相対パスで指定することも可能です。この時、ブラウザは自動的にhttp://localhost:5000/api/todos/123
へリクエストを送ります。
異なるポートやドメインにリクエストを送る場合は、明示的に絶対パスを指定する必要があります。
CORS(Cross-Origin Resource Sharing)に注意
異なるオリジン(プロトコル、ドメイン、ポートの組み合わせ)にリクエストを送信する場合、CORS の設定が必要になることがあります。サーバー側で適切な CORS ヘッダーを設定しないと、ブラウザがセキュリティ上の理由でリクエストをブロックすることがあります。
さて、そしたらやはりなぜCORSエラー出てるのかを特定せねば

あとrendererってやつ必要か?あとで調べたい
import { renderer } from './renderer'
app.use(renderer)

あー確かに、リクエストやレスポンスが実際にどんなヘッダーのせて送られてるかをchrome devtoolsのnetworkタブ(?)から見てみるか?
CORSエラーはフロントエンドで起きているように見えますが、実際にはサーバー側の設定が原因です。エラーに遭遇した場合は、サーバーのレスポンスヘッダーを確認しましょう。

めちゃくちゃ謎なのが、全オリジン・全パス許可してもだめなこと
app.use('*', cors({
origin: '*'
}))
tanstack queryを使ってた頃はできてたんだよな
今のやつはまだtanstack query使ってないただの直接fetchなのでそれが原因か?
最初tanstack query使って試した時は普通にできてたしなぁ
ということでさっきと同じようにtanstack queryを使ってfetchしてみる

ただそうなるとなぜ普通にfetchするのだとorigin許可してもエラーになるのかは気になるが・・

honoエンドポイントへのfetchと、
honoエンドポイントからsupabaseへのfetch
の2つともtanstack queryでキャッシングしないとだな

コンポーネントかカスタムフック内でしかuseParams使えないっぽいのでカスタムフック内でuseParams使ってuserId取得してそれをgetTodosの引数に渡してあげることにするか
なのでgetTodosは直接tanstack queryのqueryFnに渡すのではなく高階関数の返り値として使う
import { useSuspenseQuery } from "@tanstack/react-query"
import { todoKeys } from "../-key/key"
import { getTodos } from "./getTodos"
import { useParams } from "@tanstack/react-router"
export const useGetTodos = () => {
const { userId } = useParams({ from: '/todos/$userId/' })
const { data } = useSuspenseQuery({
queryKey: todoKeys.all,
queryFn: () => getTodos(userId),
})
return data
}

おそらくフロントは完成した(まだCORSエラーは出ている)
次はhono(バックエンド)内のsupabase-jsのfetchでもtanstack query使う

hono(バックエンド)でのfetchは、コンポーネントとか無いからサスペンスで囲むとかpendingComponentに設定するとかができないの大丈夫なんかな
クライアント側のSuspenseがそれ検知して勝手にサスペンドしてくれるのか?わからん
んでuseSuspenseQueryにすべきか?useQueryで良いか?
まぁ一応useSuspenseQueryにしとくか

ん、バックエンド側でもtanstack queryインストールしないと使えないな
ていうかコンポーネントでqueryProviderみたいなやつ初期設定しないといけなかったなぁ
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
うーん、honoでコンポーネントとか使えるのか・・?めんどくさそうだなぁどうしよう
普通にクライアント側のフォルダ内にuseQueryTodosみたいなファイル作っちゃってそれをバックエンドからインポートするだけっていうのはどうだろう、可能だろうか

できたけどエラーは変わらず。
キャッシュを見たところhonoからのqueryは実行すらされていない?でクライアントからのgetTodosしか走っておらずその途中でエラーが出たのか?
fetchingに3秒くらいなってずっとローディング中で、その後エラーになると同時にinactiveになってた
わからないのでとりあえずqueryなしの単純apiリクエスト/レスポンスを試してみる
(ユーザーテーブルに存在しないユーザーIDを指定しているのも良くないか?)

だめだ、/testというパスを作って普通にリクエスト送ってもCORSエラーだ
なぜ???

ん、honoの開発サーバ見てみたら重要そうなエラー出てるな、500エラー
あー、最初と違ってsupabase clientのファイルの場所をクライアント側ではなくバックエンド側に移してしまったかも?それによって読み取れなくなってエラー出てる?
いやでもフロント側で書こうとしたらsupabase-jsインストールしてないからエラーになる
じゃあ別にここはバックエンド側に最初からあったっぽいな
関係なさそう
デプロイしちゃえばVercelに環境変数設定できるので、これは最悪無視でも良さそうな気はする

そういえばfetchする関数内でtry catchでエラーハンドリングしてなかったからそこのエラーメッセージも見てみるか
うーんまぁ普通にFailed to fetch
だなぁ
エラーが出てもめげずに4回fetchしてるっぽく、4回目が失敗したらcatch内に書いといたthrow new Errorがやっと出るようになってる
普通に直接fetchしてみてもやっぱりだめだ、CORSエラー

なんで最初はできてたんだっけ?何が違う?
こんな単純なものすら受け取れなくなってしまった
app.get('/api/test', (c) => {
return c.text('テストだよー')
})
CORSでorigin許可するだけではだめで他に直接ヘッダー設定したりとか、リクエスト側でもfetchの際にオプション付けてヘッダー設定したりしないとダメとか?

geminiに聞く
gemini-2.5-pro-exp-03-25
CORSエラーが発生しているとのことですが、提示されたコードとエラーメッセージを見る限り、いくつかの原因が考えられます。
考えられる原因と対処法
-
ミドルウェアの適用順序:
Honoではミドルウェアは記述された順に適用されます。現在のコードでは、renderer
ミドルウェアがcors
ミドルウェアよりも先にグローバルに適用されています。// honoのindex.tsx // ... app.use(renderer) // ← これが先に適用される app.use('/api/*', cors({ // ← APIルートに対して後から適用される origin: 'http://localhost:5000', }) ) // ...
もし
renderer
ミドルウェアがリクエストを処理してレスポンスを返してしまったり、あるいは何らかの理由で処理を中断させたりすると、後続のcors
ミドルウェアが実行されず、CORSヘッダーが付与されない可能性があります。
特に、エラーメッセージに500 (Internal Server Error)
が含まれていることから、renderer
ミドルウェアか、あるいはルートハンドラ自体でエラーが発生し、CORSヘッダーが付与される前の段階で処理が失敗している可能性が高いです。対処法:
-
CORSミドルウェアを先に適用する: API関連のルートに対するCORS設定は、他のミドルウェア(特にレスポンスを返す可能性のある
renderer
など)より先に適用するのが一般的です。 -
renderer
ミドルウェアの適用範囲を見直す:renderer
がSSRやHTMLレンダリングのためのミドルウェアであれば、APIエンドポイント (/api/*
) には適用せず、HTMLを返す必要のあるルート(例:'*'
や'/'
など)にのみ適用するように変更します。
修正例:
// honoのindex.tsx (修正案) import { Hono } from 'hono' import { renderer } from './renderer' // renderer の内容を確認してください import { cors } from 'hono/cors' const app = new Hono() // 1. CORSミドルウェアをAPIルートに適用 // 可能な限り早い段階で、しかし対象パスを絞って適用するのが良いでしょう。 app.use('/api/*', async (c, next) => { console.log(`[${new Date().toISOString()}] Applying CORS for: ${c.req.path}`); // ログ追加 const handler = cors({ origin: 'http://localhost:5000', // 必要に応じて他のオプションも検討 // allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // allowHeaders: ['Content-Type', 'Authorization'], // credentials: true, // Cookie認証などを使う場合 }); await handler(c, next); // CORSミドルウェアを実行 // CORSミドルウェアがレスポンスヘッダーを追加したか確認(デバッグ用) // console.log('Response headers after CORS:', c.res.headers); }); // 2. APIルートの定義 app.get('/api/test', (c) => { console.log(`[${new Date().toISOString()}] Handling /api/test`); // ログ追加 try { // ここでエラーが発生していないか確認 return c.text('テストだよー from Hono API'); } catch (error) { console.error(`[${new Date().toISOString()}] Error in /api/test handler:`, error); // エラー時もレスポンスを返す return c.json({ message: 'Internal Server Error' }, 500); } }) // 他のAPIルートがあればここに追加 // 3. rendererミドルウェアを適用 // もしrendererがHTMLレンダリング用なら、APIルート(*)より後に、 // 具体的なパスや '*' で適用します。 // APIルートには不要なはずです。 // app.use(renderer) // ← グローバル適用をやめる app.get('*', renderer) // 例: API以外のすべてのGETリクエストに適用 export default app
-
CORSミドルウェアを先に適用する: API関連のルートに対するCORS設定は、他のミドルウェア(特にレスポンスを返す可能性のある
-
サーバーサイドでのエラー (500 Internal Server Error):
コンソールにGET http://localhost:5001/api/test net::ERR_FAILED 500 (Internal Server Error)
と表示されています。これは、CORSポリシーチェック以前に、サーバー側でリクエストの処理中にエラーが発生し、ステータスコード500でレスポンスが返された(あるいはレスポンス自体が壊れた)ことを示しています。
サーバーがエラーレスポンスを返す際、正常なレスポンスに必要なCORSヘッダー(Access-Control-Allow-Origin
など)が付与されないため、ブラウザはCORSエラーとして報告します。対処法:
- Honoサーバーを起動しているターミナル(コンソール)のログを確認し、
500 Internal Server Error
の具体的な原因を特定してください。スタックトレースなどが出力されているはずです。 - 上記の修正例のように、APIルートハンドラ内に
try...catch
を追加し、エラーハンドリングを強化します。 -
renderer
ミドルウェアの実装を確認し、意図せずAPIルートでエラーを引き起こしていないか確認してください。
- Honoサーバーを起動しているターミナル(コンソール)のログを確認し、
-
CORS設定の詳細:
単純なGETリクエストであればorigin
の指定だけで十分なことが多いですが、念のため他のオプションも確認します。-
メソッド:
GET
以外のメソッド(POST
,PUT
,DELETE
など)や、Content-Type
以外のヘッダー(Authorization
など)をリクエストで送信する場合、allowMethods
やallowHeaders
の設定が必要になることがあります。 -
Credentials: CookieやAuthorizationヘッダーを伴うリクエストの場合、サーバー側で
credentials: true
を設定し、クライアント側 (fetch) でもcredentials: 'include'
を指定する必要があります。
対処法:
- 必要に応じて
cors()
のオプションを追加します。(上記の修正例を参照)
-
メソッド:
デバッグ手順の推奨:
- サーバー側のログを確認: Honoサーバーのコンソール出力を最優先で確認し、500エラーの原因を特定・修正します。
-
ミドルウェアの順序変更: 上記の修正例のように、CORSミドルウェアを先に適用し、
renderer
の適用範囲を見直します。 -
シンプルなAPIエンドポイントでテスト: 問題を切り分けるために、他のミドルウェアの影響を受けない非常にシンプルなAPIエンドポイント(例:
app.get('/ping', (c) => c.text('pong'))
)を作成し、それにcors
ミドルウェアだけを適用して、クライアントからリクエストを送信してみます。これでCORSが通るか確認します。 -
curl
コマンドで確認: ターミナルから以下のコマンドを実行し、サーバーからの直接のレスポンスヘッダーを確認します。レスポンスヘッダーにcurl -v -H "Origin: http://localhost:5000" http://localhost:5001/api/test
Access-Control-Allow-Origin: http://localhost:5000
が含まれているか、またステータスコードが500になっていないか確認します。
まずはサーバーサイドの500エラーを解決することが、CORSエラー解消への近道だと思われます。Honoサーバーのログを確認してみてください。
なるほどなー
コンソールに GET http://localhost:5001/api/test net::ERR_FAILED 500 (Internal Server Error) と表示されています。これは、CORSポリシーチェック以前に、サーバー側でリクエストの処理中にエラーが発生し、ステータスコード500でレスポンスが返された(あるいはレスポンス自体が壊れた)ことを示しています。
サーバーがエラーレスポンスを返す際、正常なレスポンスに必要なCORSヘッダー(Access-Control-Allow-Origin など)が付与されないため、ブラウザはCORSエラーとして報告します。
リクエスト中にエラー起きて正常なレスポンス送れなくなったらCORSヘッダーを返せなくなるので、厳密にはCORSエラーではないのにCORSエラーになっちゃうってことか!
てことはさっきのinternal server errorは直さないといけないやつだったかも

あとこれは初耳だ、勉強になる
CookieやAuthorizationヘッダーを伴うリクエストの場合、サーバー側で credentials: true を設定し、クライアント側 (fetch) でも credentials: 'include' を指定する必要があります。

サーバーログのエラーのことを伝えたgeminiの返答
gemini-2.5-pro-exp-03-25
サーバーログありがとうございます!エラーの原因が明確になりましたね。
Internal server error: supabaseUrl is required.
at new SupabaseClient (...)
at createClient (...)
at C:\Users\rutob\Downloads\hono-todoapp\backend\supabase\client.ts:6:25
このログは、backend/supabase/client.ts
の中で Supabase クライアントを初期化しようとした際に、supabaseUrl
が渡されなかった(undefined
または空だった)ためにエラーが発生したことを示しています。これが原因でサーバーが 500 エラーを返し、結果としてブラウザ側では CORS ヘッダーがないというエラーに見えていたわけです。
根本的な原因:
Hono サーバー (バックエンド) のコード (backend/supabase/client.ts
) が、フロントエンド用のディレクトリ (frontend/.env.local
) に置かれた環境変数を読み込めていないためです。
-
import.meta.env
: これは Vite が提供する機能で、Vite の開発サーバーやビルドプロセスが.env
ファイルなどを読み込み、コード中のimport.meta.env.VITE_...
を実際の値に置き換えます。 -
Vite の環境変数のスコープ: Vite 開発サーバー (
vite
コマンド) は、デフォルトでは実行されたディレクトリ (今回の場合はbackend
ディレクトリ) を基準に.env
ファイルを探します。 -
今回の状況:
- Hono サーバーは
backend
ディレクトリで (おそらくvite
コマンドによって) 実行されている。 -
backend/supabase/client.ts
はimport.meta.env.VITE_SUPABASE_URL
を参照している。 - Vite サーバーは
backend
ディレクトリとその上位ディレクトリで.env
ファイルを探すが、frontend/.env.local
はその対象外。 - 結果として
import.meta.env.VITE_SUPABASE_URL
はundefined
になり、Supabase クライアントの初期化でエラーが発生。
- Hono サーバーは
解決策:
以下のいずれかの方法で、バックエンドの Hono サーバーが Supabase の環境変数を読み込めるように設定してください。
方法1: バックエンドディレクトリに .env
ファイルを置く (推奨度: 高)
-
backend
ディレクトリの直下に.env
という名前のファイルを作成します。(例:backend/.env
) -
そのファイルに、
frontend/.env.local
と同じ内容をコピーします。VITE_
プレフィックスはそのままにしておいてください (import.meta.env
を使っているため)。# backend/.env VITE_SUPABASE_URL=XXX VITE_SUPABASE_API_KEY=XXX
-
Hono/Vite サーバーを再起動します。
これで backend
で実行されている Vite サーバーが backend/.env
を読み込み、import.meta.env
に値が注入されるはずです。
方法2: Vite の設定で環境変数ファイルの場所を指定する
もし環境変数を frontend
ディレクトリで一元管理したい場合は、backend
の Vite 設定ファイル (backend/vite.config.ts
または backend/vite.config.js
) で envDir
オプションを指定します。
-
backend/vite.config.ts
(なければ作成) を編集します。 -
defineConfig
の中でenvDir
を設定します。// backend/vite.config.ts import { defineConfig } from 'vite' import honoPlugin from '@hono/vite-dev-server' // Hono用のプラグインなどがあればそのまま import path from 'path' export default defineConfig({ // Hono/Vite用のプラグイン設定などがあればここに plugins: [ honoPlugin({ entry: './src/index.tsx' // Honoのエントリーポイントに合わせてください }) // 他のプラグイン... ], // 環境変数を読み込むディレクトリを frontend ディレクトリに指定 envDir: path.resolve(__dirname, '../frontend'), // .envファイルで VITE_ 以外のプレフィックスも読み込みたい場合 (通常は不要) // envPrefix: 'VITE_', // デフォルトは 'VITE_' })
-
path.resolve(__dirname, '../frontend')
は、vite.config.ts
があるディレクトリ (backend
) から見て一つ上の階層のfrontend
ディレクトリを指します。 -
@hono/vite-dev-server
などのプラグインを使用している場合は、その設定も確認・記述してください。
-
-
Hono/Vite サーバーを再起動します。
注意点:
-
サーバーの再起動:
.env
ファイルの変更やvite.config.ts
の変更後は、必ず Hono/Vite サーバーを再起動してください。 -
.env
ファイルのコミット: Supabase の API キーなどの機密情報は Git リポジトリにコミットしないように、.gitignore
に.env
や.env.*
を追加しておきましょう。 -
as string
のリスク:import.meta.env.VITE_... as string
は、環境変数が未定義の場合にランタイムエラーを引き起こします(今回のように)。より安全にするなら、以下のように存在チェックを行うのがおすすめです。// backend/supabase/client.ts (改善案) import { createClient } from "@supabase/supabase-js"; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseApiKey = import.meta.env.VITE_SUPABASE_API_KEY; if (!supabaseUrl || !supabaseApiKey) { // サーバー起動時になるべく早くエラーを検知できるようにする throw new Error("Supabase URL and API Key must be defined in environment variables (VITE_SUPABASE_URL, VITE_SUPABASE_API_KEY)"); } // ここに来る時点で string であることが保証される (Viteが注入するため) export const supabase = createClient(supabaseUrl, supabaseApiKey);
まずは方法1を試して、Honoサーバーが正常に起動し、/api/test
エンドポイントが 500 エラーではなく正常なレスポンス(テストだよー
)を返すようになるか確認してみてください。それができれば、CORS の設定も正しく機能するはずです。
なるほどなぁ~~~
このログは、backend/supabase/client.ts の中で Supabase クライアントを初期化しようとした際に、supabaseUrl が渡されなかった(undefined または空だった)ためにエラーが発生したことを示しています。これが原因でサーバーが 500 エラーを返し、結果としてブラウザ側では CORS ヘッダーがないというエラーに見えていたわけです。
ということでbackend/ 直下にも.env.local作ったら無事直ったーーー!!!

もう不要だろと思ってフロントエンドの.env.local消したら画面が真っ白になった
フロントとバックの開発サーバのログには何もエラー出ないが、コンソールにはclientのsupabaseのエラーが出力されてる。これ↓
if (!supabaseUrl || !supabaseApiKey) {
// サーバー起動時になるべく早くエラーを検知できるようにする
throw new Error("VITE_SUPABASE_URLとVITE_SUPABASE_API_KEYが環境変数内に定義されてないとダメ")
}
んでどうやらフロントからバックのsupabase clientをインポートしてるのがまずいらしい
確かにbackend内には環境変数があるとは言え、インポート元であるfrontend側でそのsupabase clientを解決しようとするのかな、多分
そんで環境変数がfront内に見つからなくてエラー、みたいな流れだと思われる
まぁ確かにフロントで使いたいコードがバックに用意されてるのはディレクトリ設計おかしいだろというツッコミはありそうだ
確かに違和感もあるし

なのでそのbackendをimportしようとしてるfrontendのコードをbackendに移動しようとしたのだが、tanstack query使ってるからbackendでそれは使えないなぁ、、となった
backend側ではtsqueryをインストールしてないからなのもそうだが、前述の通りtanstack queryはインストールするだけでなくmain.tsx内で(コンポーネントで)セッティングする必要もあるので、それをhono側でやれるんか?みたいな話になる
できるのかもしれないけど、とりあえずバックエンドではtanstack query使わないで普通にfetchしちゃって良いとgeminiさんが言ってたのでとりあえず今回はそれでいく
gemini
おっしゃる通り、useQueryTodos
は React のフック (useSuspenseQuery
) を使っているので、そのまま Hono のバックエンド (backend/
) に移動することはできません。また、QueryClientProvider
は React アプリケーションに QueryClient のインスタンスを提供するためのコンポーネントなので、Hono サーバーサイドで直接使うものではありません。
ここで重要なのは、フロントエンドとバックエンドの役割分担を明確にすることです。
-
バックエンド (Hono):
- データベース (Supabase) と直接やり取りする。
- ビジネスロジックを実行する。
- フロントエンドからのリクエストに応じて、データを JSON などの形式で返す API エンドポイントを提供する。
-
フロントエンド (React + TanStack Query):
- UI を表示し、ユーザー操作を受け付ける。
- バックエンドが提供する API エンドポイントにリクエストを送信してデータを取得する。
- 取得したデータを TanStack Query を使ってキャッシュし、状態管理を行う (
useQuery
,useMutation
などを使用)。
現在の問題点:
frontend
の useQueryTodos
が、バックエンドのデータ取得関数 (backend/supabase/queryTodos
) を直接インポートして呼び出してしまっている点です。
正しいアプローチ (より良い方法):
-
バックエンド (Hono) に API エンドポイントを作成する:
-
backend/supabase/queryTodos.ts
は、Supabase からデータを取得するロジックだけを担当します (これは React や TanStack Query に依存しません)。 - Hono のルーター (
backend/src/index.tsx
) で、このqueryTodos
関数を使って特定のユーザーの Todo を返す API ルート (例:/api/todos/:userId
) を作成します。
-
-
フロントエンド (React) はその API エンドポイントを叩く:
-
frontend/src/.../useQueryTodos.ts
はフロントエンドに残します。 -
queryFn
の中で、backend/supabase/queryTodos
を直接呼び出す代わりに、先ほど Hono で作成した/api/todos/:userId
エンドポイントに対してfetch
リクエストを送信するように変更します。
-
具体的なコード修正例:
1. バックエンド (backend/src/index.tsx
) に API エンドポイントを追加:
// backend/src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer' // 必要に応じて
import { cors } from 'hono/cors'
import { queryTodos } from '../supabase/queryTodos' // データ取得ロジックをインポート
const app = new Hono()
// CORS設定 (APIルート用) - これは以前の修正通り
app.use('/api/*', cors({
origin: 'http://localhost:5000', // フロントエンドのオリジン
// 必要なら他のオプションも
}));
// ★★★ 新しいAPIエンドポイントを追加 ★★★
app.get('/api/todos/:userId', async (c) => {
const userId = c.req.param('userId');
console.log(`[API] Fetching todos for userId: ${userId}`);
try {
// バックエンドのデータ取得関数を呼び出す
const todos = await queryTodos(userId);
// 取得したデータをJSONで返す
return c.json(todos);
} catch (error) {
console.error(`[API Error] Failed to fetch todos for ${userId}:`, error);
return c.json({ message: 'Failed to fetch todos', error: error.message }, 500);
}
});
app.get('/api/test', (c) => {
return c.text('テストだよー from Hono API');
});
// renderer はAPI以外のルートに適用 (必要に応じて)
app.get('*', renderer);
export default app
2. バックエンド (backend/supabase/queryTodos.ts
) の確認 (例):
(これは React や TanStack Query に依存しない形になっているはずです)
// backend/supabase/queryTodos.ts
import { supabase } from './client'; // Supabaseクライアント
// Todoの型定義 (例)
export interface Todo {
id: number;
user_id: string;
task: string;
is_completed: boolean;
created_at: string;
}
export const queryTodos = async (userId: string): Promise<Todo[]> => {
console.log(`[DB] Querying todos for userId: ${userId}`);
if (!userId) {
throw new Error('User ID is required');
}
// Supabaseからデータを取得するロジック
const { data, error } = await supabase
.from('todos') // 'todos' は実際のテーブル名に合わせてください
.select('*')
.eq('user_id', userId) // user_id カラムで絞り込み
.order('created_at', { ascending: true });
if (error) {
console.error('[DB Error] Supabase query failed:', error);
throw new Error(`Supabase query failed: ${error.message}`);
}
console.log(`[DB] Found ${data?.length ?? 0} todos for userId: ${userId}`);
return data || []; // データがない場合は空配列を返す
};
3. フロントエンド (frontend/src/routes/todos/$userId/-functions/useQueryTodos.ts
) の修正:
// frontend/src/routes/todos/$userId/-functions/useQueryTodos.ts
import { useSuspenseQuery } from "@tanstack/react-query";
import { todoKeys } from "../-key/key"; // Query Key
// import { queryTodos } from "~/backend/supabase/queryTodos"; // ← これは削除!
// ★★★ APIを叩くための新しい関数を作成 (別ファイルでもOK) ★★★
const fetchTodos = async (userId: string): Promise<Todo[]> => { // Todo型は適切に定義・インポート
console.log(`[Fetch] Fetching todos from API for userId: ${userId}`);
// Hono の API エンドポイントを叩く
const res = await fetch(`http://localhost:5001/api/todos/${userId}`); // ポート番号確認
if (!res.ok) {
const errorData = await res.json().catch(() => ({ message: 'Failed to parse error response' }));
console.error(`[Fetch Error] API request failed: ${res.status}`, errorData);
throw new Error(`API request failed: ${errorData.message || res.statusText}`);
}
// JSONレスポンスをパースして返す
const data: Todo[] = await res.json();
console.log(`[Fetch] Received ${data.length} todos from API`);
return data;
};
// ★★★ useQueryTodos フック ★★★
export const useQueryTodos = (userId: string) => {
const { data } = useSuspenseQuery({
queryKey: todoKeys.queryTodos(userId), // userId もキーに含めるのが一般的
// queryFn は API を fetch する関数に変更
queryFn: () => fetchTodos(userId)
});
// data の型が Todo[] になる (fetchTodos の返り値の型)
return data;
}
// Todo 型の定義 (例 - backend と共有するか、フロントで再定義)
interface Todo {
id: number;
user_id: string;
task: string;
is_completed: boolean;
created_at: string;
}
// Query Key の定義 (例 - key.ts ファイル)
// export const todoKeys = {
// all: ['todos'] as const,
// queryTodos: (userId: string) => [...todoKeys.all, userId] as const,
// }
回答まとめ:
-
Hono で
QueryClientProvider
は不要。 これは React フロントエンドのものです。 -
Hono で TanStack Query の React フック (
use...
) は使えない。 バックエンドは API を提供する役割。 -
より良い方法: バックエンド (Hono) は DB アクセスロジックとそれを公開する API エンドポイントを作成。フロントエンド (React + TanStack Query) はその API を
fetch
し、useQuery
などで状態管理・キャッシュを行う。 -
クライアントとバックエンド両方で使うべきか?:
-
クライアント (React): API フェッチとキャッシュのために
@tanstack/react-query
を 使うべき です。 -
バックエンド (Hono): 通常、TanStack Query は不要です。単純な
async/await
や Supabase クライアントで十分です。
-
クライアント (React): API フェッチとキャッシュのために
この構成にすることで、フロントエンドとバックエンドの関心事がきれいに分離され、それぞれの役割に集中できます。
これでサスペンスが効かないからuseSuspenseQuery使いたいやらキャッシュしたいやらというニーズが出てきたらまたその時方法を調べるとする

ということでディレクトリ整理しつつ書いていくか
backend側のtanstack query不要なので全部消しちゃって良かったりとかその辺り

なんか

なんかimportを補完からやるとこうなるけど、これだとエラーになる
import { queryTodos } from 'supabase/CRUD/queryTodos'
普通に相対パスでちゃんと指定してあげるとエラー解消できた
import { queryTodos } from '../supabase/CRUD/queryTodos'

よし、渡されたuserIdに一致するtodo一覧を取得する処理はできた
んで恐らく必要だと思われるのでfrontend/にもbackend/にも同じ.env.loval配置した
もう後は問題無くどんどん書いていけそうだ、ゴールが見えてきた