📖

プロジェクトのお気に入りポイントを解説

2024/02/12に公開

この記事は2023年にスタートしたプロジェクトの振り返りです。小さなチームで開発を進めて、ほぼすべての設計プロセスに関わったと思います。このプロジェクトの構成に満足しているのでお気に入りポイントについて記事を書きたいと思いました。

まずプロジェクトの構成を簡単に紹介します。そしてお気に入りポイントについて書きます。

ディレクトリ構成

モノレポ構成でturbopack(npm)を採用しています。パッケージ管理にはpnpm、モノレポツールにはNxも試しましたが最終的に一番安定している組み合わせを選択しました。

ルートパッケージにインストールしているのは全体的なサポートツールに絞っています。

  • turbopack
  • husky
  • prettier
  • scaffdog
  • sort-package-json

ワークスペースは3つに分けています。

  • apps: アプリ(とりあえずspaパッケージのみ)
  • functions: Lambda
  • packages: その他

packagesには次のパッケージを置いています。

  • cdk: CDK用(TypeScript)
  • database: prisma用
  • models: APIのInput, Outputを型安全にする(tRPC的な)

アーキテクチャー

  • フロントはReact(SPA)をCloud Front + S3に配置
    • Cloud FrontにはWAFを設定
  • API Gateway + Lambda(VPC)でサーバレスなREST APIを構成
    • 1Resouce:1Lambda (Proxyリソースを使わない)
  • Aurora Serverless v2 (PostgreSQL)

フロントはReact(SPA)をCloud Front + S3に配置

今回はAWS縛りのプロジェクトでした。

最初はReact(SPA)ではなくNextjs(SSR)をAmplify Stackでデプロイしておりました。当時NextjsのApp Directoryがリリースされていましたが、まだ安心して使えないだろうと考えてpagesディレクトリベース(getServerSidePropsを使う)で進めていました。

しかし実際にデプロイされたものを触ってみると、Amplify Stackで構築されるものはVercelのようにNextjsに最適化された環境ではないため致命的に遅かったです。そのためプロジェクトの途中でNextjs=>Reactに乗り換えました。NextjsのファイルベースルーティングをReact Router(v6)で置き換える作業が発生しただけで、それ以外の作業は発生しませんでした。pagesベースで進めていてラッキーだったところだと思います。

技術スタック

  • React
  • MUI
  • React Hook Form(以下、RHF)
  • Zod
  • React Query
  • Axios
  • Prisma
  • Jotai(少し)

おまけ

  • date-fns
  • ts-pattern
  • just-*

設計

データ関連の命名ルールは以下の通りです。

  • Zodで定義したスキーマはxxxSchema
  • APIで扱う型は
    • リクエストの型はXxxInput
    • レスポンスの型はXxxOutput
  • RHFで扱うスキーマはxxxFormDataSchema、型はXxxFormData

API周りのスキーマを次のように共有しています。

modelsワークスペースに置かれるものはZodによるスキーマ定義やモデルに関連するビジネスロジックです。共有することが前提なのでブラウザーでもNodejsでも動くコードだけにしておきます。また関数型プログラミングのスタイルで書いているので安心感があります。フロントとサーバーのコードに同じ型を使用できるので、スキーマに修正が生じた場合でもスムーズに変更点を洗い出すことができます。

React Hook Form(以下、RHF)のリゾルバーに使用するスキーマはAPIに渡すスキーマとは別にしておきます。同じにしてしまうと、フォーム側の事情に引きずられて歪んだAPI設計になってしまう可能性があるからです。また、歪んだAPI設計にならないよう気を配っていても実装が複雑化することも考えられます。リクエストデータが複雑なケースでは、RHFをハックするようなコードになってしまうことがありメンテナンス性に下げることになります。

それぞれのスキーマを分けて、APIから受け取ったデータをフォームで扱える構造に変換する関数、フォームで管理されたデータをAPIのリクエストデータに変換する関数をそれぞれ用意することで、しっかりとした関心の分離が実現し、コードをシンプルにしておくことができます。

開発の流れ

PrismaにBookというモデルを追加する例で2つのケースを紹介します。

findManyで取得したデータを表示する

  • PrismaでBookというスキーマ定義を追加
  • functions/booksを作成して、index.ts, list.tsを作成
  • list.tsにprisma.book.findMany({})まで書く
  • modelsパッケージにfindManyに渡すselectオブジェクトを定義
    • selectBookとしてas const付きで定義
  • modelsパッケージにBookOutput型を定義
    • Prisma.BookGetPayload<typeof selectBook>で定義
  • prisma.book.findMany({ select: selectBook })として使用
  • apps/spaパッケージでBookOutputをaxiosのレスポンスの型に指定
  • React Query + Axiosでデータを取得して表示

ポイント

modelsパッケージの型宣言に使用しているPrisma.BookGetPayload<typeof selectBook>は、prisma.book.findMany({ select: selectBook })のレスポンスの型と一致します。
そのBookOutput型をaxiosのレスポンスの型に指定することでフロントエンド ← バックエンドの型安全が実現します。

createする

  • functions/booksにcreate.tsを作成
  • prisma.book.create({ data })まで書く
  • modelsパッケージにdataのスキーマをbookInputSchemaとして定義
  • z.infer<T>BookInput型も用意
  • dataに渡す値をconst data = bookInputSchema.parse(...)でバリデーションする
  • apps/spaパッケージでbookFormDataSchemaを定義
  • bookFormDataSchemaをReact Hook Form(RHF)のリゾルバーにする
  • z.infer<T>BookFormData型も用意
  • BookFormDataBookInputの変換ロジックを作成してRHFのsubmitで適用
  • React Query + AxiosでuseMutationを定義する

ポイント

modelsパッケージにbookInputSchema,BookInputを定義しており、bookInputSchemaをバックエンド、BookInputをフロントエンドで使用することで型安全を実現してます。

お気に入りポイント

プロジェクトの構成や開発の流れを簡単に説明しました。
次はお気に入りポイントを書きたいと思います。

型安全

T3と同じですがTypeScriptによって隅々まで型チェックが有効な構成にできたのは気持ちいいですね。この点は Prisma + Zod が大活躍していると思います。不安なくコードが書けます。変更できます。

流行りのスタックを試せた

私の観測範囲で新しくて人気のあるスタックを使っています。流行っているかどうかは技術選定の本質ではありませんが、日々新しく登場する技術スタックを検証する時間には限りがあるので人気のあるスタックをとにかくプロジェクトで導入したことで強制的に学習することが出来ました。

以下の3つは、それぞれの目的で代わりとなるライブラリーを使ったことがないものでした。

  • React Hook Form
  • React Query
  • Prisma

どれも便利ですね。一回使ってしまうと代替ライブラリーに乗り換えることはあっても使わない開発が想像しにくくなりますね。

ただしReact Hook Formだけは要注意だと思っています。

すべてを使いこなそうとすると学習コストが高いです。大体のケースで楽をさせてくれるのですが、少しでも複雑なケースでReact Hook Formを使おうとすると実現が難しかったり、実現してもメンテナンスしたいと思えないコードになってしまうことがあります。

React Query, Prismaは文句なしにまた使いたいと思えるライブラリーでしたね。

今回採用できなかったものは以下の通りです。

  • Nextjsのデプロイ先にVercel
  • DB は Supabase or PlanetScale
  • フロントとバックエンドの型安全にtRPC
  • vanilla-extract or Tailwind

別の機会にチャレンジできたらと思います。

フォーム、キャッシュ周りのコードがスッキリ

React Hook Form、React Queryを使用することで、以前はReduxを使用してたくさん書いていたコードをかなり減らせたと思います。今回のプロジェクトで状態管理にはJotaiを使用していますが、これら2つのライブラリーのおかげでJotaiを使う機会はすごく減りました。

オレオレフレームワーク的なコードが少ない

アプリケーションに共通する悩み事には、それを解消するためのライブラリーがほぼ100%存在します。
「共通する悩み事」の境界を定義するのは難しいことですが、Zod, React Query, Jotai といったライブラリーを適切に使用してアプリ固有のロジックに集中できたと思います。

MUI + React Hook Form のコンポーネント実装方法はいくつかの方針が考えられるので、その辺りでオレオレっぽさを感じるかもしれません。しかし薄いコンポーネントなのでリファクタリングしやすく、大きな負債になることはないと思います。

IaC

今回CDK(TypeScript)を使ってインフラ定義をしましたが、非常に使い勝手がよかったです。
CloudFormation / Terraform よりも読みやすいと感じます。型チェックによる定義ミスを検出してくれる点、分からないときにホバーで表示されるリンクからドキュメントにジャンプできる点が使用していて気持ちいいです。

まとめ

0からアプリ構築できる機会は非常に多くの学びがありますね。
2024年も、そんな機会に恵まれますように。。

Discussion