👻

Full-Stack TypeScriptの最終到達点、T3-Turboで新規開発した話

2024/12/06に公開

はじめに

この記事はTSKaigi Advent Calendar 2024の記事です。(TSKaigiの運営メンバーとして最初の記事になりそうです。)
https://qiita.com/advent-calendar/2024/tskaigi

みなさん、TypeScript書いてますか?最近、フロントエンドもバックエンドも(加えてインフラも)TypeScriptで統一する、Full-Stack TypeScriptを採用する事例が増えてきました。
Full-Stack TypeScriptの由来やメリットはこちらの記事から
https://zenn.dev/ascend/articles/full-stack-typescript

フロントもバックもTypeScriptだし、どうせならMonorepoで管理しようということで、TypeScript専用のMonorepo管理ツールであるTurborepoを導入し、T3-Turboというアーキテクチャを導入することになりました。
T3-Turboというアーキテクチャは、以前導入を考えた際に記事にしているので、こちらをご参照ください。
https://zenn.dev/ficilcom/articles/t3-turbo-architecture
またT3-Turboのリポジトリは以下になります。
https://github.com/t3-oss/create-t3-turbo

本記事では、T3-Turboアーキテクチャを導入し、ある程度開発が進んだので、振り返りの意味も含めて改善案を紹介したいと思います。

振り返り

ディレクトリ構成

現在のディレクトリ構成はこちら

t3-turbo
|   - apps
|     | - mobile    //Expo
|     | - web       //NextJS
|     | - api       //NestJS
|   - packages
|     | - tsconfig  //TypeScript
|     | - database  //Prisma(+Zod)
|     | - ui        //UI Component(shadcn)
|     | - graphql   //GraphQL
|     | - domain    //Domain
|   - package.json
|   - pnpm-lock.yaml
|   - turbo.json

各フレームワークの選定

  • Mobile → Expo
  • Frontend → NextJS
  • Backend → NestJS

TypeScriptならこれというもので、モジュールを無闇に増やしたくないのと、プロダクトの性質上肥大化しやすいので、全体的にいわゆる「重め」のフレームワークを採用しています。(もちろん異論は認める)
それぞれ豊富な採用実績があるので、フレームワークのせいで機能開発に困ったということは特にありませんでした。

改善案

ここでは開発を進めているうちに導入することになった改善案を紹介します。

pnpm → bun → pnpm

bunが1.0リリースしたことでpnpmからbunに切り替えました。しかし、Workspace機能が弱いことや、肥大化してくるとドキュメント通りに動かないバグを踏むことが多々あり(Bunが直接の原因かどうかわからない)、機能開発に集中するためにpnpmに戻しました。

packages/database

Turborepoの公式ドキュメントに推奨されていることと、豊富な採用実績があるPrismaを選択しました。加えてprisma-zod-generatorという便利なGeneratorが用意されており、Validationを最小限の工数でここに書くことができます。
https://github.com/omar-dulaimi/prisma-zod-generator
packages内にValidationを定義したことで、フロント/バックやフレームワークを意識することなく、最小限の依存で開発できるようになります。

packages/ui

Component数が豊富になったこと(執筆時点で50種類くらい)、v0の登場でshadcn/uiが扱いやすくなりました。(shadcn/uiはWebのみ対応)
将来的に管理画面やLP作成の際に共通化するため、UI Componentをpackages内に分離しました。

packages/graqhql

NextJSとExpoで.graqhqlファイルを共有することから、packages内で共通化しています。graphql-codegenによって生成したTSファイルも、packages内に置くことで、どのモジュールからも呼び出せるようにしています。

packages/domain

フレームワークに依存しない、ドメインロジックをここに書いています。appsモジュールからは使う際に依存性を注入して呼び出すだけです。これはやっていてめちゃくちゃ良かった案です。以下の利点を享受できます。

  • ドメインロジックをフレームワークの依存から剥がせる。(バージョンアップや、リファクタリングの影響を受けなくなる)
  • ロジックをどこに書くか迷わなくなる。ドメインロジックの実装に集中できるため、開発体験ヨシ。
  • RSCによってClient/Serverの境界が曖昧になった現在こそ、特に効果がいい

NestJSでCQRS設計パターンを実現する

こちらもやっていてめちゃくちゃ良かった案です。モダンCQRSについては以下の記事をご参照ください。
https://zenn.dev/ficilcom/articles/cqrs-modernization

結論から申し上げますと、

  • Query → /graqhql
  • Command → /trpc

というエンドポイントに分けています。

Query(GraphQL)

Queryについては、PostGraphileというGraphQLによって実現しています。PostGraphileについては以下の記事で紹介しています。
https://zenn.dev/ficilcom/articles/invitation-to-postgraphile
PostgreSQLに接続すると、GraphQLエンドポイントが自動生成されてしまう優れもの。(今までCRUDのコードをたくさん書いたのはなんだったのか)
ポチポチするとgqlが勝手に発行されます。(; ・`д・´) ナ、ナンダッテー!!
騙されたと思って是非使って欲しいです。

Command(tRPC)

自動生成で対応できなかったものに関してはtrpcを使って別のエンドポイントを用意しています。今年になってnestjs-trpcが正式にリリースされました。

https://www.nestjs-trpc.io/

こちらもフロント/バックでTSの型を共有できるので、最上の開発体験を味わえます。

CQRSを使ってみて

サービスにもよると思いますが、ほとんどQueryで書けると思います。Validationを別のpackage(zod)に分離しているので、複雑なロジックでなければバックエンドを書く必要もありません。複雑なロジックが必要な場合でもtRPCがあるので安心です。
体感として、今の開発ではバックエンドの9割近くをQueryで実装できています。

おわりに

TypeScriptに統一したならではのメリットを開発体験にも活かしました。参考になったや、もっといい方法があるなど教えてくれると幸いです。
最後までお読みいただき、ありがとうございました。

TSKaigiの告知

2025年5月23日/24日にTSKaigi 2025が開催されます!
TSKaigiは日本最大級のTypeScriptをテーマとした技術カンファレンスです(前回の参加者2000人以上)
TypeScriptに興味のある方は、ぜひ公式サイトやXを確認してみてください。
公式サイト:https://tskaigi.org/
X:https://x.com/tskaigi

フィシルコム

Discussion