Next、supabaseでプロジェクト作ってみる

目標は
- アプリアイディアが思いついたら爆速でプロトタイプができる構成
- サーバーサイド(データフェッチ先)を容易に切り替え可能な構成
トレードオフになる2要素だが、後者重視でいいバランスを見つけたい

技術はnext, supabaseで固定。
選定理由は
- 小規模であればvercel, supabaseでインフラコストが0なこと
- ネットの情報の多さ(主観)
- supabase, vercelの支払いが一本化されたこと
- 個人的な触ってみたさ

アーキテクチャイメージ(仮)
思想
- BEサーバー(図
BackendLayer
)はサービスの成長とともに切り出す前提。- UI→BackendLayerの通信はGraphQLにしたいところだが、サーバーが切り分けられない都合上断念。スキーマ(型定義)だけ使う案も考えたがざっと調べた感じ、積極的に取り組んでいる参考実装がなかった。
詳細
- CQRS意識でリードモデルとライトモデルは分けたい。
- ここで開発速度は落ちるのは許容。(生成AIあるし、対して開発速度変わらんやろ。)
- Mutation側はDDDの戦術的設計を導入
- どうせ移行するならトランザクションスクリプトでいいやんという指摘にはぐぅのねもでないが、個人的な学習のためこうさせてもらう。
- CQRSやるならデータストアをQueryとMutationで分けたかったが、こちらは初速の構築・開発に時間がかかりすぎるので断念。また、要件次第で(結果整合性・即時反映したいのか否かあたりで)アーキテクチャが変わりうるため、個人開発複数アプリで再利用したいという目標に沿わない。あとお金がかかる。
- BEサーバー(図
BackendLayer
)はいつでも移行できるよう、RSC(UI層)と明確に境界を設ける-
RSC ← (REST API) → Qeury
の(REST API)
は実際はただの関数呼び出しに過ぎないが、移行に備え、REST APIを意識したI/Oとする。
-
- RSCから直接PostgRESTを呼び出しても良いが、腐敗防止層としてQueryサーバー(図
Query(ACL)
)をかませる。- Dataloaderの仕組みがsupabaseのPostgREST APIになければ(多分ないので)Queryサーバーで用意する。
- Query側のリードモデルはDBスキーマそのままとするのが一番柔軟なのでは?という思想のもとで、全て自動生成で済ませられるようにPostgRESTを採用。
- supabaseのドキュメント読む限りTSの型生成もできるし、型情報の上書きもできるため、柔軟性は十分ありそう。

モジュール設計・ディレクトリ構造
- UI領域の設計はこちらを参考にする
https://zenn.dev/akfm/books/nextjs-basic-principle/viewer/part_2_container_1st_design - Query側はDataloader次第だが基本的にDB・ORMに密に依存していてOK
- Mutation側はDDDの戦術的設計を利用。Presentation/Application/Domain/Infra layerに分ける

ライブラリ調査メモ
-
mui
- スキップ
-
tailwind
- スキップ
-
shadcn
-
ドキュメント:https://ui.shadcn.com/
This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.
コンポーネントライブラリではなくて再利用可能なコンポーネントをコピペしてくれる。
-
「コンポーネントの機能は作るから、見た目は好きなように作って」みたいなイメージか
-
スタイルの流行はtailwind
-
-
zod
- github:https://github.com/colinhacks/zod
Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple string to a complex nested object.
- TypeScriptのゾッとする話 ~ Zodの紹介 ~
- フォームのバリデーションに限ったライブラリではなかった。
- スキーマ(+フィールドバリデーション)を定義できるライブラリっぽい。
- github:https://github.com/colinhacks/zod

諸調査
typescriptの型関連
基本構造的部分型だが、privateなフィールドがあれば交渉型になる
newtype-tsとやらを使っている↓ 基本Privateにすればいいか。何がベスプラなんだろ? ↓これみると専用のprivateフィールド用意している

一旦調査はここまでにしてとりあえず手を動かしながら考える。
まずは環境構築
とりあえずnext supabaseの連携ができている環境を作る。
- Dashboardからcreate new project
- create next app
$ node -v
v22.8.0
$ npx create-next-app -e with-supabase tour-planing
Need to install the following packages:
create-next-app@14.2.11
Ok to proceed? (y) y
Creating a new Next.js app in /Users/horisho/devs/tour-planing.
Downloading files for example with-supabase. This might take a moment.
Installing packages. This might take a couple of minutes.
added 212 packages, and audited 213 packages in 29s
38 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created tour-planing at /Users/horisho/devs/tour-planing
Inside that directory, you can run several commands:
npm run dev
Starts the development server.
npm run build
Builds the app for production.
npm start
Runs the built app in production mode.
We suggest that you begin by typing:
cd tour-planing
npm run dev
npm run dev
でこれ
3. 環境変数定義
project作ったらドキュメント保管してくれるのアツい
Env設定したら開発画面も変わった
画面に出てる通りサインアップしたら確認メールまで届いた。すげえ
ここまでぽちぽちしてるだけ

初日の目標
- Query側を一気通貫させる曳光弾を通す
- スタイルは行わない。プレーンなデータを表示するだけにとどめる。

PostgRESTの設定
デフォルトで作成されているっぽい
テーブル作成
DashboardのGUIからもしくはSQLEditorのいずれかから作成可能。
Localの開発はどうしようかな?
- 一人だったらLocal用のSupabaseサーバー立てるのが便利だけど、チームだとどうなるんだ?
- 手元にPostgresサーバー用意することになるのかな?スキーマはどう同期する?
この辺りにヒントありそう↓
ローカルにPostgresのDockerコンテナを立てるCLIが用意されてる!?すげえ
その他メモ
- defaultのデータ型にULIDなさそうだな。UUIDはある。
https://supabase.com/docs/guides/database/tables#:~:text=Show/Hide default data types - primary keyの自動挿入もあるっぽい。UUID系もやってくれるのか?
We could also use generated by default as identity, which would allow us to insert our own unique values.

supabase CLIでローカル開発
この手順に従って進めてみる
npx supabase init
defaultだとStorageサービスやらGraphQLエンドポイントも自動で立ち上がるのでconfig.tomlの修正が必要そう
めちゃくちゃDockerImageダウンロードされるので注意
色々同期したコミット
ローカルのnext開発環境からローカルのsupabaseプロジェクト(postgresコンテナ)にアクセスするには?
↓配下にドキュメントがない。
NEXT_PUBLIC_SUPABASE_URL
, NEXT_PUBLIC_SUPABASE_ANON_KEY
をそれぞれ、supabase start
で出てきた、API URL
とanon key
を追加すればOK
これ探すので時間かなり潰れた 😢
config.toml
でenable_confirmations = false
となっているのでプロダクションと違い、メールは来ない。承認不要でログインできる。
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"

テーブル作成
参考
primary keyを何にするか?
ulidが使い慣れているが特にこだわりはない。
uuid型にTableエディタからデコードされたulidを入れられないのでuuid(v7)にすることにした。
db resetするたびにusers消える問題
seed作る上でもusersは消したくない or usersをseedで作る。
dumpしたデータを入れるのはありだがauth周りでgithubに上げて良いものとダメなものがわからん。(開発の情報なので気にせずあげても良いが)
supabase migration up --local
でいけた
手順
supabase migration new create_plans_table
- SQL書く
CREATE TABLE plans (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
name TEXT NOT NULL
);
-
supabase db reset
orsupabase migration up --local
- table editorから適当なサンプルデータを入れる。

初日の目標に達するためにコードを書いていく
UIレイヤ
↓の思想に則ってディレクトリを作成
queryレイヤ
npx supabase gen types --lang=typescript --local --schema public > query/database.types.ts
でテーブル構造のスキーマを吐き出す。
型定義の利用
調査supabase.jsによるRESTAPIリクエストはキャッシュされるのか?
前提
nextはfetch APIを拡張し、Request Memoizationによって同一エンドポイントへのリクエストはキャッシュされる。
キャッシュの有効期限はコンポーネントのレンダリングが終わるまで。(レンダリング開始から終了までトランザクションが貼られていたとするとのリードコミット相当の分離レベルをデフォルトで実現)
Duration
The cache lasts the lifetime of a server request until the React component tree has finished rendering.
supabase.jsにおけるfetchはこの対象になるのか?
ここ読む限り大丈夫かな?調査するより試すほうが早そう。
できてなさそう。
→まあContainerコンポーネントで一度しかリクエスト投げない想定なので問題ないとするか。調査は手隙にやろう。(わんちゃんエラったからキャッシュできていないだけかも)
dataloader
TODO

初日終えて雑感想
(眠いので支離滅裂)
- とにかくsupabaseがすごい
- 1コマンドでローカルのsupabaseプロジェクト(postgreコンテナ、オブジェクトストレージ(s3)コンテナ、etc,)立ち上がったのえぐい。同期も1コマンドでできるし、差分マイグレーションも楽そう。
- REST API(PostgREST)がdefaultで作成されるのすごい。スキーマの自動生成もできるし。
- ただ、supabase.jsが用意したAPIクライアントが極めて扱いづらい(おそらくPostgRESTの生成するAPIが扱いづらい)ので、普通にORM用意した方が生産性よくなる説。
- Log & Analytics見れるし。
- これを全機能無料で使えるのエグすぎる。(pricing)
- Authentication気づいたらできてたわ。
- ts「型の再利用」できるの良い。
- ただ、痒いところに手が届かないのでtype-fest使って見てる。
- ただ、こちらも絶妙に痒いところに手が届かない
- 多分TSのプロたちは標準のものを組み合わせてうまく目的物を組み立てるんだろうな(知らんけど)
- ただ、こちらも絶妙に痒いところに手が届かない
- ただ、痒いところに手が届かないのでtype-fest使って見てる。
- (ts)構造的部分型と公称型うまく使い分けられるからレイヤ感の詰め替えサボれていい感じ
- optionalなフィールドの定義どうしようかな。。。
- UUID(v4/v7), ULID関連毎度何がなんだったのか忘れる。
- こちらのBookマジでありがとうございます。NextやろうにもPageRouterやらAppRouterやらRSCやらでどこから触れればわからなかった状態を救ってくれた。今のカオス()なNextがどう言った背景で生まれたのかとともに、今のベスプラまで体系的に学べて大変参考になっています。
- UI関連のディレクトリ構成はもう単一責任(SRP)重視でFeatures Directoryがデファクトスタンダード(仮)くらいになったのかな?
- Atomic Designは消えたか。。。
- ユーザのアクセスできるリソースかのバリデーションどうかけるのが正解なんだろうか。
- フォーマッタが効いてないな。biomeを使う

Container/PresentationalパターンやりつつClient Boundaryを意識するの頭パンクする。
Container側をClientComponentにした方が扱いやすいパターンもある気がしてきた
↑この表をどこまで守るかだな。。。
あとは、Shared ComponentsとClient Componentsをディレクトリから明確に分けたほうが良いか?
ボトムアップにコンポーネント実装して行った方が良さそうかも
あとは、URL設計見直してLayoutとうまく組み合わせればいける気もした。

→普通に責務ごとにコンテナ設計して、データフェッチはContainer、UIはPresentationにするのがシンプルだな。