Zenn
Open79

Hono練習

Yug (やぐ)Yug (やぐ)

データベースとORM系どっちも知識皆無なので、全部担ってくれるSupabaseに頼る

honoのapi書くファイル内でsupabaseのjsコード書いてdbアクセスして、て感じでいけそう

apiエンドポイントがあるとは言えさすがにクライアントルーティングは別で用意する必要がありそうなのでtanstack routerも使うことになると思う

Yug (やぐ)Yug (やぐ)

ディレクトリ構成はこうなりそう

project
├──front
│    └──src    // React
└──back
    └──index.tsx    // Hono & Supabase

frontでcreate vite、backでcreate honoするか

Yug (やぐ)Yug (やぐ)
  • フロントエンド
npm create vite@latest frontend

https://ja.vite.dev/guide/#最初の-vite-プロジェクトを生成する

  • バックエンド
npm create hono@latest backend

https://hono.dev/docs/getting-started/basic#starter

honoのテンプレートはcf pagesで良いか(よくわからんが)

PagesとWorkersは機能的にも似通っていてややこしいのですが、ざっくりとPagesは静的コンテンツ向けのホスティングサービスで、シンプルなデプロイや配信に最適で、Workersは動的な処理やサーバーレスアプリに特化した、より高度なカスタマイズが可能なサービスです。

https://www.spread1.co.jp/wp/post-2727/

バックエンド側でsupabaseインストール

cd backend/
npm i @supabase/supabase-js

https://www.npmjs.com/package/@supabase/supabase-js

Yug (やぐ)Yug (やぐ)

メモ

  • Honoのインスタンス作る時に使用するBindingsって型はなんだ

https://hono.dev/docs/getting-started/cloudflare-pages#use-bindings-in-your-application

  • middlewareっていうのが何なのかわかってない
    • 認証とか?でもtanstackのbeforeLoadでsupabase-js書けば認証できちゃうな
  • jsxをhonoから返せるっぽい(今回それやるかはわからん)
Yug (やぐ)Yug (やぐ)

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

Yug (やぐ)Yug (やぐ)

c.renderしちゃうとjsxがそのままレンダーされて画面表示されちゃう気がするので、ただレスポンスしてほしいな

レスポンスはどうやってやるんだ?

geminiに聞いた。return c.textでいけるのか

app.get('/', (c) => {
  return c.text('はじめる')
})
Yug (やぐ)Yug (やぐ)

んでそれを受け取るフロント側も初めて書くがやってみよう

  • まずfetchで得る型はPromise<Response>ぽい
  • それをresponseとして受け取ったら、更にresponse.text()までしてやっと生のテキストとして使える

text() は Response インターフェイスのメソッドで、 Response ストリームを取得して完全に読み込みます。文字列で解決するプロミスを返します。

https://developer.mozilla.org/ja/docs/Web/API/Response/text

今回honoからはreturn c.text()でテキストとして返してるから、jsonとして解釈しようとするrespose.json()は使えないのでresponse.text()使うってことか

json() は Response インターフェイスのメソッドで、 Response のストリームを取得して完全に読み取ります。本体のテキストを JSON として解釈した結果で解決するプロミスを返します。

なお、このメソッドは json() という名前であるにもかかわらず、結果は JSON ではありません。入力として JSON を取って解釈し、 JavaScript のオブジェクトを生成します。

https://developer.mozilla.org/ja/docs/Web/API/Response/json

Yug (やぐ)Yug (やぐ)

フロント側でCORSエラーが出たのでHono側でCORSコードを書く必要もありそうだ

用意されてる、見つけた
https://hono.dev/docs/middleware/builtin/cors

originをhttp://localhost:5000/にしてあげるとかでいけそう

あとTanStack Queryとか使って、use()に渡すpromiseをキャッシュしろというのをよく聞くのでその導入もやりたい
キャッシュしないとレンダリングごとに毎回fetchして新しくpromise作っちゃうから非効率とかそういう理由だと思う(そんでそこから下手すると無限レンダリングに繋がったりするのかも?)
https://speakerdeck.com/diggymo/reactnouse-tutehe-nan-6af6d25f-ac0d-4e61-b0db-c11b1737e906?slide=15

Yug (やぐ)Yug (やぐ)

よし、Honoで以下ミドルウェア書いたら画面描画が理想通り機能した

app.use(
  cors({
    origin: 'http://localhost:5000',
  })
)

https://hono.dev/docs/middleware/builtin/cors#usage

だがコンソールにエラーは出ている

まさにこれだな
https://speakerdeck.com/diggymo/reactnouse-tutehe-nan-6af6d25f-ac0d-4e61-b0db-c11b1737e906?slide=15

キャッシュしてないpromiseが見つかったら自動でエラー出して「キャッシュしろ!」て警告を出すReactの仕様だろう

同じくuse()使ってSupabaseのAPI叩いたときは何もエラー出なかった理由は、supabaseのDB APIにはすでに全origin許可のCORS設定が書かれているのだろう

あーやっぱそうっぽい、これだ。なるほど

Yug (やぐ)Yug (やぐ)

とりあえずTanStack Queryを導入してキャッシュしていく

TanStack Query編

npm i @tanstack/react-query

https://tanstack.com/query/latest/docs/framework/react/installation#npm

npm i @tanstack/react-query-devtools

https://tanstack.com/query/latest/docs/framework/react/devtools#install-and-import-the-devtools

supabaseコードもawait使うような非同期処理なのでbackend/内にもインストールすべきか?

いやでも...

Suspense機能させたい

use()使うしかない

tanstack query導入してキャッシュせねば

という流れなので、クライアント側でSuspenseしちゃえばバックエンドの非同期待ちは拾えるから普通にSuspense機能させることができるのでバックエンドではtanstack query不要か

まぁでもbackend/ではキャッシュできなくなるからちょっと非効率にはなるのかも
なので余裕があったらbackend/でも導入しよう

とりあえずfrontend/で導入していく

Yug (やぐ)Yug (やぐ)

まずこれ読んでReactQueryDevtools書く
https://tanstack.com/query/latest/docs/framework/react/devtools#floating-mode

んでこれ読んでQueryClientProviderとかそのあたり書く
https://tanstack.com/query/latest/docs/framework/react/quick-start

既にtanstack routerのRouterProviderもあるのでその同階層にQueryClientProviderを書けば良いのか親に書けば良いのかわからないが以下を参考にすると親で良さそう
https://zenn.dev/calloc134/articles/6680b272a2c2c5#:~:text=まず TanStack Query を初期化するため

公式にも同じように書かれてたのでやはり親で良さそう(なぜqueryの方が親になるのかは気になるが)
https://tanstack.com/router/v1/docs/framework/react/examples/basic-react-query

<QueryClientProvider client={queryClient}>
  <RouterProvider router={router} />
</QueryClientProvider>,
Yug (やぐ)Yug (やぐ)

useSuspenseQueryというのがあるっぽいがこれ使えばuse()使わずに普通にawait使ってfetchしてもSuspense機能しそう...!!!?めっちゃ助かるぞ
https://zenn.dev/calloc134/articles/6680b272a2c2c5

無限レンダリング回避するためuse()使うコンポーネントとfetch()使うコンポーネント分割しないといけなくていろいろ面倒だったがそれが不要になるのか

いやtanstack query導入したらその分割も不要になる気はするからそれは関係ないかも...
まぁuse()使わずにfetchだけで良くなるのはどっちみち助かる

そしてuseQueryとほぼ同じ使い方っぽい、素晴らしい
https://tanstack.com/query/latest/docs/framework/react/reference/useSuspenseQuery

注意点もあるっぽいので一応メモ
https://zenn.dev/frontendflat/articles/tanstack-suspense-request-waterfall

Yug (やぐ)Yug (やぐ)

ディレクトリ設計どうすべきだろうか

ん-これ通りservice/というのを作るとtanstack routerで既にパスとして分離できてるディレクトリと被る気がするなぁ
https://zenn.dev/taisei_13046/books/133e9995b6aadf/viewer/ae97a1#service層の役割を解説

こっち参考にして、普通に-functions/っていうの作ってそこに全部突っ込むか
https://zenn.dev/aishift/articles/ad1744836509dd#tanstack-routerのディレクトリ設計

Yug (やぐ)Yug (やぐ)

とりあえずここ以降を参考にして同じように実装してみる
https://zenn.dev/taisei_13046/books/133e9995b6aadf/viewer/ae97a1#function.ts

できた!!use()使わずにawaitしてるだけなのにSuspense機能してる!useSuspenseQuery凄い!
エラーも消えてる


だが1つ気になる点がある

tanstack queryでisPendingの状態を取得する必要があるのかどうかわからない

tanstack routerの方でpendingComponent: で設定しているので絶対重複してるんだよな
どうすれば良いだろう、どっちを使えば良いんだろう

Yug (やぐ)Yug (やぐ)

試してみたら、isPendingの方は完全に機能していないことがわかった
tanstack routerのpendingComponentしか機能してない

pendingComponentを消してisPendingのコンポーネントだけ表示させようとしたらそもそもpending中は画面真っ白になってSuspense機能してなかった

更に....useSuspenseQueryではなくuseQueryにしてみたのだが、それでもなぜか普通にSuspense機能してる

なぜ??気になるがとりあえず飛ばす。pendingComponentを書けばokということをとりあえず暗記する。そして一応useSuspenseQueryの方を使っていくことにする

Yug (やぐ)Yug (やぐ)

違いはdataの状態だけであって、どっちもSuspenseに対応できているという点は同じなのかもしれない(?)

通常のuseQueryとuseSuspenseQueryの大きな違いはdataがundefinedな状態を考慮する必要がなくなったことです。

https://zenn.dev/aishift/articles/ad1744836509dd#tanstack-queryのsuspense

だからuseSuspenseQuery使うならisPendingもisErrorも返り値として受け取る必要は無くなってdataだけ受け取れば良いっぽい。アツい

Yug (やぐ)Yug (やぐ)

とはいえもはやReactの<Suspense></Suspense>をまったく使用していないのにSuspenseが効いてるのがなぜかわからず混乱してるのだが、そこから考えられることは...

pendingComponentが全部やってくれてる。最強。

ということ。

だがまぁそれだけという訳でもなく、useQuery/useSuspenseQueryを使っていることが前提という部分も大事か。

なぜなら普通にコンポーネント内でawaitするのは当然不可。クライアントコンポーネントはawait禁止なので

んでごり押しでuseEffect内でasync関数作ってその中でawaitしてそれを実行しようとしてもそれはSuspense機能しない。

サスペンスはエフェクトやイベントハンドラ内でデータフェッチが行われた場合にはそれを検出しません。

https://ja.react.dev/reference/react/Suspense#displaying-a-fallback-while-content-is-loading

なのでtanstack queryみたいに隠蔽してくれるやつ使わないと無理だよね、という話はある

だがそれさえ使っちゃえばもう<Suspense>使わずとも勝手にSuspense機能してpendingComponentがfallbackとして表示されるようになる

それでいいのか?<Suspense>の存在意義めっちゃ薄くない?という混乱はあるが、とりあえずめっちゃ楽に書けるという事実は変わらないので助かる

ん-、まぁReactが将来を見越しての話なのかなー?という予想

tanstackとかのライブラリを全く使わずReact単体でもuse()と<Suspense>だけ使えば非同期やらキャッシュやらを完全に対応できるようになるかもしれないという未来があり、そのReactオンリーという栄光のために残してある、ということなのかもしれない(?)

Yug (やぐ)Yug (やぐ)

さて、todos/パスとか本格的に作っていきたい

のでsupabaseでtable作ったりsupabase-jsをhonoの中で書いたりしていくか

Yug (やぐ)Yug (やぐ)

ていうかjsxをhonoから返す予定は無いのでこのrenderer.tsxっていう規定のファイル不要かも?

backend\src\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からミドルウェアとして呼び出してるっぽいんだけれども

backend\src\index.tsx
import { renderer } from './renderer'

app.use(renderer)

まぁいいや

Yug (やぐ)Yug (やぐ)

Supabase設定編

この辺り見つつ、supabaseのプロジェクト初期化、接続までやっていく
https://supabase.com/docs/reference/javascript/initializing

backend/に.env作成
https://ja.vite.dev/guide/env-and-mode#env-variables

前ハマったやつ。アクセスポリシー作らないとfetchしてもtableから空の配列しか返ってこないので注意

ここで設定できる

checkは無しで消して、usingの中をtrueにすれば良い

textかvarcharか...違いはあんま無さそうなのでとりあえずvarcharで
https://lets.postgresql.jp/documents/technical/text-processing/1#:~:text=text と varchar の比較

foreign keyも設定し、updateもdeleteもcascade(滝のように流れる=連動する=一心同体)で設定
todos.id -> users.idにした。userが消えたらtodoも全部消えるというロジックが理想なので、userが主従の主であるべき


毎回ちゃんとemail認証かけるの面倒だし1時間に3通までしか送れないのでとりあえずConfirm emailはオフにしとく

あとはsupabase.auth.signUp()すればAuthentication -> usersに勝手に登録される

Yug (やぐ)Yug (やぐ)
  • 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()

https://supabase.com/docs/reference/javascript/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()

https://supabase.com/docs/reference/javascript/auth-getuser

ということでgetUser使うか

Yug (やぐ)Yug (やぐ)

いやでも使い分けすべきかも?

  • getUser()でアカウント登録つまりsignupが済んでいるのか確認できる
  • getSession()でログイン中つまりsignin中が確認できる
    • ただ改ざんされる可能性はあるというのが面倒だな(今回は無視するか...)

いや違うな、どっちもセッション確認してログイン中かどうか見てるっぽいのでどっちも役割同じだ

アカウント作成済みかどうかみたいな情報に関してはそもそも不要だし不可能かもしれん

なぜなら、何も入力されてない状態でどうやってアカウント存在確認をするんだという話になるから

メアド打ってくれればそれを基にアカウント作成済みか確認できるが、セキュリティ的にそれは良くなさそうだし、何よりそんな手間をユーザーに求めるなら最初からユーザーに新規登録させてエラーで「既にアカウント登録済みです」って出すのと何ら変わらないのでそれで良くないかという感じがした

なので新規登録ボタンを出すべきかログインボタンを出すべきかというのを自動検出してどっちかだけを画面に出すみたいなことは不可能 & 不要だと判断したのでやらないことにする

Yug (やぐ)Yug (やぐ)

envが読み取れない、初見のエラーだ

Uncaught Error: supabaseUrl is required.
  • VITE_prefixはつけてる
    • import.meta.env.○○で読み取ろうとしてる
  • .envの階層をbackend/でもルートディレクトリでも試した
  • サーバー立てなおした
  • .gitignoreに指定するのも指定しないのも試した

Hono使ってるからhono側のenv設定方法使わないといけないとか?

Yug (やぐ)Yug (やぐ)

あーやっとわかった

frontend/直下に.env設置しないとだった。Viteが入ってるのはfront/のみなのでそのディレクトリ内に入れないといけないということなのだろう

ルートディレクトリでもbackend/でもダメだがfrontend/ならokというところは盲点だった

知見:Viteがインストールされているpackage.jsonの同階層に.envを置く!

Yug (やぐ)Yug (やぐ)

追記

ここの認識がまだ甘かったのでめっちゃ痛い目を見た

んでbackendも同じようにvite入ってるよな・・?とは思う

supabase clientを使いたいファイルと同ディレクトリ内(backend/かfrontend/)の直下(もしくは親ディレクトリの直下でも良い?)に.env置かないとだめだった

なので兄弟ディレクトリ(?)の直下に置いちゃうと読み取れずにinternal server errorになって詰む

Yug (やぐ)Yug (やぐ)

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.

セッションに期限切れのアクセストークンがある場合、このメソッドは新しいセッションを取得するためにリフレッシュトークンを使用します。

だとするとややセキュリティ上良くなさそうではあるが(?)、まぁいい

Yug (やぐ)Yug (やぐ)

ぐわー、またNavbarのコンポーネント内でuseEffect, useState使ってログイン状態をawaitで取得してsessionがあったらログアウトボタンを表示するという長いコードを書いてしまっている

ラグが視覚的に顕著だしコードも見づらいからできればuseEffect書きたくないんだよなぁ、わざわざ中にasync関数を作ってそれを実行するというのも面倒だし
んでuseEffect使うならuseStateもセットで使わざるを得なくなりどんどんコード量が増えていく

とにかくこれ良くない気がする

NavbarTop.tsx
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の勉強なのでここは飛ばす
(まぁ機能自体は実現できてるので問題は無い)

Yug (やぐ)Yug (やぐ)

index.tsx内にコンポーネントも一緒に書いちゃうならRoute.useParams()でいけるんだけど、コンポーネントは別ファイルに分離したいので、そうなるとRouteを参照することができない

だがuseParamsの引数fromを入れれば解決するっぽい

const { userId } = useParams({ from: '/todos/$userId/' })

よし、todos/$userIdでuserIdを取得できてることを確認した

Yug (やぐ)Yug (やぐ)

ちなみに何となく"signin"ってわかり辛いよなぁという感があるのでユーザへの表示は"ログイン"にしてる

ユーザに表示されないコード部分はsigninにしてるが(supabaseの用意された関数名もsignInだし)

Yug (やぐ)Yug (やぐ)

新しい知見を得た

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 プロパティに受け入れられる型と一致しないと判断しているためです。

この問題を解決するには、以下のいずれかの方法を試してみてください:

  1. 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
    })
  }
}
  1. パスを作成する際にルートパラメータオブジェクトを使用する(推奨):
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 というパスが定義されていて、文字列テンプレートリテラルではなく、このような形式で指定する必要があります。

Yug (やぐ)Yug (やぐ)

今mockで/apiにリクエスト送ってそれを受け取るというのをtanstack query使って実装できてるのでそれを参考に書いていく

honoのディレクトリ構成もちゃんとやりたい
https://zenn.dev/yodaka/articles/ad49f29a54ceba

あとsupabaseのディレクトリは自分なりに考えて作っていくか

Yug (やぐ)Yug (やぐ)

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);

実装のポイント

  1. Honoのバックエンド:

    • POSTエンドポイントを使用してuserIdを受け取ります
    • Supabase-jsを使って、そのuserIdに関連するTODOを取得します
    • エラーハンドリングと適切なHTTPステータスコードを返します
  2. Reactのフロントエンド:

    • TODOデータのfetchと状態管理
    • userIdをリクエストボディに含めてAPIにPOSTリクエスト
    • 新しいTODOの作成機能も含んでいます

実際の実装では、認証システムと連携させてユーザーIDを動的に取得する必要があります。また、環境変数の設定や適切なCORS設定も忘れないようにしてください。

何か具体的な質問や機能追加があれば、お知らせください。

Yug (やぐ)Yug (やぐ)

requestの受け取りはc.req.XXXでいろいろできそうだ
https://hono.dev/docs/api/request

今回はidをpath paramsにしているのでc.req.paramを使う感じかな

app.get('/api/todos/:id', async (c) => {
  const id = c.req.param('id')
})

どうせ中でsupabase-jsをawaitで実行することになるからasyncは必須になるだろう

Yug (やぐ)Yug (やぐ)

次にreactからのリクエスト方法を調べる

todos/$userIdのbeforeLoadでuserId取得してそれをリクエストとして送りたい

fetchのオプションでmethod, headers, bodyを設定したりするのかな
んで今回はそれをtanstack query使ってやる必要あるから難しくなりそうだ

Yug (やぐ)Yug (やぐ)

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 コンポーネントが表示されます。

この仕組みを使って、認証状態やユーザーデータなどをルートロード時に取得し、それをコンポーネントに直接提供することができます。

試してみる

Yug (やぐ)Yug (やぐ)

というかtodos/$userIdのpathでbeforeLoadするとき、userId自体はpathからそのまま取得できそうだな

んでそのuserIdを基にtodosデータ取得して、それを返却してcomponentにpropsとして渡す、という流れが妥当か

Yug (やぐ)Yug (やぐ)

ん、これもしかして、TanStack Queryでfetchをキャッシュするという行為が不要になるか?

Supabaseのクエリをsupabase-jsで書くわけだがこれは別にfetchでもないしなぁ

まぁでもPromiseを返すという点では同じ。どうすべきだろうか

Yug (やぐ)Yug (やぐ)

ん?なんかいろいろミスってた

react側で全部supabase-jsのクエリ書いたりとかやろうとしてた、honoでクエリは実行せねば

流れ

react側のtanstack queryでhonoの/api/todos/:userIdをfetch

honoでsupabase-jsのクエリ実行してその結果としてのtodosを返却

それをreactが受け取り表示

Yug (やぐ)Yug (やぐ)

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

Yug (やぐ)Yug (やぐ)

なんか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/:userIdfetch する場合:

この場合は、異なるポート番号 (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エラー出てるのかを特定せねば

Yug (やぐ)Yug (やぐ)

あとrendererってやつ必要か?あとで調べたい

import { renderer } from './renderer'
app.use(renderer)
Yug (やぐ)Yug (やぐ)

あー確かに、リクエストやレスポンスが実際にどんなヘッダーのせて送られてるかをchrome devtoolsのnetworkタブ(?)から見てみるか?

CORSエラーはフロントエンドで起きているように見えますが、実際にはサーバー側の設定が原因です。エラーに遭遇した場合は、サーバーのレスポンスヘッダーを確認しましょう。

https://zenn.dev/crebo_tech/articles/article-0015-20241117#よくあるcorsエラーの例

Yug (やぐ)Yug (やぐ)

めちゃくちゃ謎なのが、全オリジン・全パス許可してもだめなこと

index.tsx
app.use('*', cors({
  origin: '*'
}))

tanstack queryを使ってた頃はできてたんだよな

今のやつはまだtanstack query使ってないただの直接fetchなのでそれが原因か?
最初tanstack query使って試した時は普通にできてたしなぁ

ということでさっきと同じようにtanstack queryを使ってfetchしてみる

Yug (やぐ)Yug (やぐ)

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

Yug (やぐ)Yug (やぐ)

honoエンドポイントへのfetchと、
honoエンドポイントからsupabaseへのfetch

の2つともtanstack queryでキャッシングしないとだな

Yug (やぐ)Yug (やぐ)

コンポーネントかカスタムフック内でしかuseParams使えないっぽいのでカスタムフック内でuseParams使ってuserId取得してそれをgetTodosの引数に渡してあげることにするか

なのでgetTodosは直接tanstack queryのqueryFnに渡すのではなく高階関数の返り値として使う

useGetTodos.ts
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
}
Yug (やぐ)Yug (やぐ)

おそらくフロントは完成した(まだCORSエラーは出ている)

次はhono(バックエンド)内のsupabase-jsのfetchでもtanstack query使う

Yug (やぐ)Yug (やぐ)

hono(バックエンド)でのfetchは、コンポーネントとか無いからサスペンスで囲むとかpendingComponentに設定するとかができないの大丈夫なんかな

クライアント側のSuspenseがそれ検知して勝手にサスペンドしてくれるのか?わからん

んでuseSuspenseQueryにすべきか?useQueryで良いか?
まぁ一応useSuspenseQueryにしとくか

Yug (やぐ)Yug (やぐ)

ん、バックエンド側でもtanstack queryインストールしないと使えないな

ていうかコンポーネントでqueryProviderみたいなやつ初期設定しないといけなかったなぁ

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

https://tanstack.com/query/latest/docs/framework/react/quick-start

うーん、honoでコンポーネントとか使えるのか・・?めんどくさそうだなぁどうしよう

普通にクライアント側のフォルダ内にuseQueryTodosみたいなファイル作っちゃってそれをバックエンドからインポートするだけっていうのはどうだろう、可能だろうか

Yug (やぐ)Yug (やぐ)

できたけどエラーは変わらず。

キャッシュを見たところhonoからのqueryは実行すらされていない?でクライアントからのgetTodosしか走っておらずその途中でエラーが出たのか?

fetchingに3秒くらいなってずっとローディング中で、その後エラーになると同時にinactiveになってた

わからないのでとりあえずqueryなしの単純apiリクエスト/レスポンスを試してみる
(ユーザーテーブルに存在しないユーザーIDを指定しているのも良くないか?)

Yug (やぐ)Yug (やぐ)

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

なぜ???

Yug (やぐ)Yug (やぐ)

ん、honoの開発サーバ見てみたら重要そうなエラー出てるな、500エラー

あー、最初と違ってsupabase clientのファイルの場所をクライアント側ではなくバックエンド側に移してしまったかも?それによって読み取れなくなってエラー出てる?


いやでもフロント側で書こうとしたらsupabase-jsインストールしてないからエラーになる
じゃあ別にここはバックエンド側に最初からあったっぽいな

関係なさそう

デプロイしちゃえばVercelに環境変数設定できるので、これは最悪無視でも良さそうな気はする

Yug (やぐ)Yug (やぐ)

そういえばfetchする関数内でtry catchでエラーハンドリングしてなかったからそこのエラーメッセージも見てみるか

うーんまぁ普通にFailed to fetchだなぁ

エラーが出てもめげずに4回fetchしてるっぽく、4回目が失敗したらcatch内に書いといたthrow new Errorがやっと出るようになってる

普通に直接fetchしてみてもやっぱりだめだ、CORSエラー

Yug (やぐ)Yug (やぐ)

なんで最初はできてたんだっけ?何が違う?

こんな単純なものすら受け取れなくなってしまった

index.tsx
app.get('/api/test', (c) => {
  return c.text('テストだよー')
})

CORSでorigin許可するだけではだめで他に直接ヘッダー設定したりとか、リクエスト側でもfetchの際にオプション付けてヘッダー設定したりしないとダメとか?

Yug (やぐ)Yug (やぐ)

geminiに聞く

gemini-2.5-pro-exp-03-25

CORSエラーが発生しているとのことですが、提示されたコードとエラーメッセージを見る限り、いくつかの原因が考えられます。

考えられる原因と対処法

  1. ミドルウェアの適用順序:
    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
    
  2. サーバーサイドでのエラー (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ルートでエラーを引き起こしていないか確認してください。
  3. CORS設定の詳細:
    単純なGETリクエストであれば origin の指定だけで十分なことが多いですが、念のため他のオプションも確認します。

    • メソッド: GET 以外のメソッド(POST, PUT, DELETE など)や、Content-Type 以外のヘッダー(Authorization など)をリクエストで送信する場合、allowMethodsallowHeaders の設定が必要になることがあります。
    • Credentials: CookieやAuthorizationヘッダーを伴うリクエストの場合、サーバー側で credentials: true を設定し、クライアント側 (fetch) でも credentials: 'include' を指定する必要があります。

    対処法:

    • 必要に応じて cors() のオプションを追加します。(上記の修正例を参照)

デバッグ手順の推奨:

  1. サーバー側のログを確認: Honoサーバーのコンソール出力を最優先で確認し、500エラーの原因を特定・修正します。
  2. ミドルウェアの順序変更: 上記の修正例のように、CORSミドルウェアを先に適用し、renderer の適用範囲を見直します。
  3. シンプルなAPIエンドポイントでテスト: 問題を切り分けるために、他のミドルウェアの影響を受けない非常にシンプルなAPIエンドポイント(例: app.get('/ping', (c) => c.text('pong')))を作成し、それに cors ミドルウェアだけを適用して、クライアントからリクエストを送信してみます。これでCORSが通るか確認します。
  4. 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は直さないといけないやつだったかも

Yug (やぐ)Yug (やぐ)

あとこれは初耳だ、勉強になる

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

Yug (やぐ)Yug (やぐ)

サーバーログのエラーのことを伝えた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 ファイルを探します。
  • 今回の状況:
    1. Hono サーバーは backend ディレクトリで (おそらく vite コマンドによって) 実行されている。
    2. backend/supabase/client.tsimport.meta.env.VITE_SUPABASE_URL を参照している。
    3. Vite サーバーは backend ディレクトリとその上位ディレクトリで .env ファイルを探すが、frontend/.env.local はその対象外。
    4. 結果として import.meta.env.VITE_SUPABASE_URLundefined になり、Supabase クライアントの初期化でエラーが発生。

解決策:

以下のいずれかの方法で、バックエンドの Hono サーバーが Supabase の環境変数を読み込めるように設定してください。

方法1: バックエンドディレクトリに .env ファイルを置く (推奨度: 高)

  1. backend ディレクトリの直下に .env という名前のファイルを作成します。(例: backend/.env)

  2. そのファイルに、frontend/.env.local と同じ内容をコピーします。VITE_ プレフィックスはそのままにしておいてください (import.meta.env を使っているため)。

    # backend/.env
    VITE_SUPABASE_URL=XXX
    VITE_SUPABASE_API_KEY=XXX
    
  3. Hono/Vite サーバーを再起動します。

これで backend で実行されている Vite サーバーが backend/.env を読み込み、import.meta.env に値が注入されるはずです。

方法2: Vite の設定で環境変数ファイルの場所を指定する

もし環境変数を frontend ディレクトリで一元管理したい場合は、backend の Vite 設定ファイル (backend/vite.config.ts または backend/vite.config.js) で envDir オプションを指定します。

  1. backend/vite.config.ts (なければ作成) を編集します。

  2. 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 などのプラグインを使用している場合は、その設定も確認・記述してください。
  3. 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作ったら無事直ったーーー!!!

Yug (やぐ)Yug (やぐ)

もう不要だろと思ってフロントエンドの.env.local消したら画面が真っ白になった

フロントとバックの開発サーバのログには何もエラー出ないが、コンソールにはclientのsupabaseのエラーが出力されてる。これ↓

if (!supabaseUrl || !supabaseApiKey) {
  // サーバー起動時になるべく早くエラーを検知できるようにする
  throw new Error("VITE_SUPABASE_URLとVITE_SUPABASE_API_KEYが環境変数内に定義されてないとダメ")
}

んでどうやらフロントからバックのsupabase clientをインポートしてるのがまずいらしい

確かにbackend内には環境変数があるとは言え、インポート元であるfrontend側でそのsupabase clientを解決しようとするのかな、多分
そんで環境変数がfront内に見つからなくてエラー、みたいな流れだと思われる

まぁ確かにフロントで使いたいコードがバックに用意されてるのはディレクトリ設計おかしいだろというツッコミはありそうだ
確かに違和感もあるし

Yug (やぐ)Yug (やぐ)

なのでその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 などを使用)。

現在の問題点:

frontenduseQueryTodos が、バックエンドのデータ取得関数 (backend/supabase/queryTodos) を直接インポートして呼び出してしまっている点です。

正しいアプローチ (より良い方法):

  1. バックエンド (Hono) に API エンドポイントを作成する:

    • backend/supabase/queryTodos.ts は、Supabase からデータを取得するロジックだけを担当します (これは React や TanStack Query に依存しません)。
    • Hono のルーター (backend/src/index.tsx) で、この queryTodos 関数を使って特定のユーザーの Todo を返す API ルート (例: /api/todos/:userId) を作成します。
  2. フロントエンド (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 クライアントで十分です。

この構成にすることで、フロントエンドとバックエンドの関心事がきれいに分離され、それぞれの役割に集中できます。

これでサスペンスが効かないからuseSuspenseQuery使いたいやらキャッシュしたいやらというニーズが出てきたらまたその時方法を調べるとする

Yug (やぐ)Yug (やぐ)

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

Yug (やぐ)Yug (やぐ)

なんかimportを補完からやるとこうなるけど、これだとエラーになる

import { queryTodos } from 'supabase/CRUD/queryTodos'

普通に相対パスでちゃんと指定してあげるとエラー解消できた

import { queryTodos } from '../supabase/CRUD/queryTodos'
Yug (やぐ)Yug (やぐ)

よし、渡されたuserIdに一致するtodo一覧を取得する処理はできた

んで恐らく必要だと思われるのでfrontend/にもbackend/にも同じ.env.loval配置した

もう後は問題無くどんどん書いていけそうだ、ゴールが見えてきた

ログインするとコメントできます