Postgres + Prisma + zodによる、SSOTかつE2E型安全な技術スタックについての振り返り
Typescript/node.jsのWEBアプリ開発において、DBからフロントエンドまでを、通貫でSSOTかつ型安全にしようとした際の所感です。
大学のサークル活動で開発したNext.jsのwebアプリlounas.jpの設計時に、型安全性を考慮した開発を目指しました。
その際に使用した技術スタック、実際にコーディングしたときの所感を書こうと思います。
冒頭は諸条件をつらつら書いているので、本筋から読みたい方は[ 技術スタックへの所感 ]まで読み飛ばしていただければ幸いです。
技術スタック
まずはおおまかな構成と責任範囲から。
- node.js: 実行環境
- Next.js ^13.5.6: 本体。フロントエンドのルーティング、バックエンドAPI定義、静的ページ生成など
- React: レイアウト・UIコンポーネント
- PostgreSQL: データベース、認証(...Supabaseが処理)
- Prisma: 独自スキーマによるマイグレーションおよび、型安全なクエリが書けるORM
- zod: 型安全化・バリデーション
- zod-prisma-types:
schema.prisma
にzodのバリデーションを記述することができ、zodのschema
を自動生成してくれる。
- zod-prisma-types:
※他にも、メール送信やCDNのための外部サービスが積まれていますが、主題と外れるため今回は割愛しました。
アプリ概要
次に、アプリのユースケースと規模感は以下の通りです。ここでは要点のみを挙げ、UIの見た目や包括的なアプリ紹介はlounas.jpのランディングページからご覧いただければ幸いです。
- 大学生活での昼食体験を、よりよくするアプリ。
- 新宿西口周辺はオフィス街で、一見どんなお店があるのかよくわからない。地図アプリ等は杓子定規な結果しか得られない。
- 自分たちで周辺地域を調査し、学生に安心しておすすめできるラインナップをアプリに掲載した。
- おすすめ機能:「定番vs個性」「量」「価格帯」をタップで選ぶと、絞り込まれたメニューからおすすめを5品表示する。その日の気分・シーンに合わせた食事を探せる。
- ターゲットは我々の所属大学の学生のみに絞り、客層の行動データとして有意なものにするよう仕向けた。
- 新宿西口周辺はオフィス街で、一見どんなお店があるのかよくわからない。地図アプリ等は杓子定規な結果しか得られない。
- UXへのこだわり: 体験重視、ユースケースに最適化。
- おすすめ機能の絞り込み画面: 細かい絞り込み条件指定UIは煩雑なので、選択肢は大きなボタンをタップすることで選ぶような画面にした。
- ユーザーが求めているのは店舗ではなく料理であるので、店舗の一覧ではなく料理を出すようにした。
- 道案内機能: 入り組んだ新宿地下街では地図があっても中々迷う。写真付きの道案内を、全店舗で掲載した。
- 履歴: 訪れたお店を記録し、ホーム画面に月間開拓数を表示。食べたことのない料理を発掘してもらうよう誘導した。
以上の要件を、議論やスケッチを重ねて洗練させていきました。
経緯
今回、先に述べたようなスタック構成にしたのは、型安全性だけでなく"Single Source of Truth"を重視するべきと判断したためです。(対訳だと「信頼できる唯一の情報源」などと言われているものです)。
データフロー設計の観点から、経験上こういったデータ定義にありがちなのが、1つ変えたらコードベースの修正箇所が4つに増える…という肌感覚がありました。
コーディングするメンバーが4人と少人数だったため、このようなスキーマの摺合せ作業に作業コストは割けないだろうな、という予感のもと開発が始まりました。
一方で、開発チームは:
- WEB開発未経験 (Windowsアプリは経験有) の自分
- WEB開発経験者 が1人 (今回最も実装で手を動かしてくれたメンバー)
- アプリ開発未経験者 が2人
- UI/UXデザイナー が1人
- PM が1人
という構成でした。このように入門者が多かったため、型安全にしてガードレールを敷く判断が魅力的に映りました。
自分自身はというと、設計を担当しているのにも関わらずweb開発は素人で、ASP.NETとNext.jsのプロトタイプをいくつか作った程度でした。web開発における動的型付けの世界なんて概念的に知っている程度で、それらが持つ強みなんていうのはあまり実感として理解していなかったのではないかと、今では思います。さらに、C#/WPF畑で浮世離れした個人開発をしていたので、型=正義の信条もありました。
技術スタックへの所感
本題です。実装に着手したときの感想・浮かんだ問題点などを挙げていきます。
SSOTはとても快適。しかし、痒いところに手が届かなくなる
まず、prismaの優秀さが挙げられます。prisma.schema
ファイルでテーブルを定義することで、DBのマイグレーションを簡易かつ安全に行うことができます。
その際、スキーマに応じたCRUD処理用のTypeScriptの関数を自動生成してくれます。これにより、入出力データが型チェックされることで、DBを用意する前から型の不具合がないことが担保できました。
また、zod-prisma-typesパッケージが、ボイラープレートの大海原になるであろうzodschema
の定義を自動生成してくれたことも大きいです。
テーブルから型をそのまま書き起こしたものだけでなく、default
値のあるフィールドやJOIN
で結合するフィールドが、含まれている版/オミットされている版など、通常だと膨大な組み合わせのschema
を用意する羽目になりますが、このパッケージによって自動化されます。
さらに、prisma.schema
上にzodのバリデーションルールを書いておくだけで、フロントバック双方で共通となるバリデーションをデータ定義のすぐ横に書いておけるという、ミス防止にもつながる利点もありました。
ただし、こういった外部ライブラリを使えば使うほど、コードベースの思想が偏っていき、既存の規格との相性が悪くなります。今回のスキーマでは、DBのRow Level Securityが設定できません。また、DBが置いてあるSupabaseのトリガーアクションもこのSSOTの管轄から外れてしまいます。(例えば、ユーザー新規登録時にレコードを認証DBとアプリDBの双方に追加する処理)
こうなると、一貫性を重視しすぎて密結合になっているのでは?という指摘ができます。一つのライブラリの仕組みが他の全体と互換性がないと、変なハックをする羽目になったり、コンセプトそのものが瓦解することになりかねません。
次からは、ライブラリやサービス仕様に依存しない設計をすることで、スムーズな開発が行えるよう考案できればいいなと思います。
変更コストは削減したが、イニシャルコストが膨らんだ
今回の目的はまずリリースすることであって、変更への耐性や将来性をそこまで考慮するべきだったのか、という視点を持つべきでした。リリースしてユーザーからのフィードバックを得ることが第一目標になったため、安定性よりも早く出すことを先決にするべきでした。また、前の項目で述べたデメリットが足かせとなり、開発スピードの低下を招いた側面があることを否めません。
具体的には、型安全にするためのzodschema
を書かなければいけないことがあり、これが少し煩雑でした。DBのテーブルに対応したスキーマはzod-prisma-typesが自動生成してくれるものの、実際のリクエスト/レスポンスでやりとりするときのペイロード用スキーマを用意しなければなりません。このアプリの規模感であれば、この辺はjsお得意のダックタイピングで切り抜けても良かった気がします。
結論:規模感にあった技術選定をしたい
ソフトウェア開発の設計においては、大規模開発をするための複雑な戦略的スタックと、小規模開発をするためのスピーディな戦術あるのだと痛感しました。WEB分野で動的型付けやダックタイピングが有難がられているのかを、身を以て体験できました。
appendix: 初めてチーム開発をやってみた感想
今回作成したアプリは、まだまだ一人前と呼ぶには遠い機能ラインナップであり、開発もスムーズだったとは言い切れないところがありました。しかし、それよりもまずリリースできたことが何よりの成果だったと思います。最後まで作り上げることで、どういう部分で引っかかるのか、チームメンバーの得意不得意が浮き彫りになりました。また、ユーザーに使っていただいてフィードバックを得ることで、想像ベースでない指針も持つことができました。
本記事は以上です。ここまでお読みいただきありがとうございました!
nafell
大学生、26卒エンジニア志望。アプリ開発サークルを設立し、lounas.jpのバックエンド・DB設計を行いました。2年後期にインターンで設計の手法(要件定義~詳細設計)を学び、IoTシステムの調査・開発を行いました。