はじめに
Web APIの開発手法としてはREST API, GraphQLあたりがメジャーな手法かと思いますが、2021年ごろからtRPCが話題になってきました。
特長としてはこんな感じです。
- バックエンドのTypeScriptの型定義をフロントエンドにそのまま持ち込める
- スキーマと実装が乖離する心配がない
- スキーマ生成の手間がない
- RESTとGraphQLのいいとこどりをしている
- RESTと違って取得系APIのパラメータにJSONが使える
- GraphQLと違ってドメインモデルを反映したグラフの構築が不要で高度なキャッシュ機構もないので、学習コストが低い
- GraphQLと違って取得系APIがデフォルトでGETメソッドなので、CDNなどでキャッシュできる
実に素晴らしいフレームワークに見えますが、個人的には一部のケースを除いて業務で使うべきではないと考えています。この記事では、
について述べていきます。
使うべきでないと考えている理由
tRPCの大きな特徴として、REST (OpenAPI) やGraphQLと異なり、間に挟まるスキーマが存在せず、TypeScriptの型定義を直接共有しているという点があります。
間に挟まるスキーマがなくなることで、以下のようなメリットがあります。
- スキーマと実装が乖離する心配がない
- スキーマ生成の手間がない
- スキーマの都合で型情報が失われない
一方で、スキーマがないことは以下のようなデメリットも生みます。
- TypeScript以外の言語を使うことが難しくなる
- フロントエンドとバックエンドが密結合になる
ここで、これらのメリット/デメリットを比較してほしいのですが、メリットよりデメリットの方が影響の範囲が大きく、致命的です。メリットは使用するライブラリやコーディング規則さえあればRESTやGraphQLでも代替できるレベルのものですが、デメリットの方は開発体制全体が狭い範囲に縛られてしまい、脱却も難しいものです。メリットとデメリットについて1つずつ説明していきます。
スキーマと実装が乖離する心配がない
拙い実装が行われているサーバーでは、スキーマと実装が乖離するということが往々にして起こります。例えばnullableでないフィールドがnullになるとか、スキーマに存在しない(返してはいけない)フィールドが返るとかです。
しかし、これは実装が悪いのであって、スキーマが悪いわけではありません。
スキーマと実装の乖離は、スキーマからコードを/コードからスキーマを自動生成することである程度回避することができます。高品質な自動生成ツールを選定し、スキーマと実装が乖離するようなコードを書かなければ、スキーマと実装が乖離する可能性は十分低くできます。
スキーマ生成の手間がない
スキーマ生成にコマンドを叩く必要があったとしても、一般にビルドなどよりずっと高速に回りますし、ビルドステップなどに組み込めるケースも多いです。スキーマ生成忘れなどはCIで回避できます。
スキーマの都合で型情報が失われない
スキーマでサポートされていない型があるとすれば、その型はある程度特殊な型ということであり、そもそもそのような型を使わないということを検討する必要があります。
例えば、複雑なユニオン型はOpenAPIでもGraphQLでも完全にはサポートされていませんが、そもそもそのようなユニオン型を返すよりも、型ごとに別のフィールドで返すようにした方が筋がいいことが多いです。型定義に条件分岐が入っているような場合は、そのようなフィールドを別の型に切り出すなどの対応を考えましょう。
TypeScript以外の言語を使うことが難しくなる
フロントエンドもバックエンドもTypeScriptに縛られます。TypeScript以外でも使うことはできますが、それなりのペインを伴います。
フロントエンド
例え最初はWebだけで提供する想定だったサービスでも、サービスが拡大してモバイルアプリなどを提供することになる可能性というのは十分考えられます。
アプリケーションがtRPCを使っているとなったら、モバイルアプリはTypeScriptで書けるフレームワークを使うか、スキーマから各言語の型を再構築するかという2択を迫られます。
モバイルアプリで使うフレームワークとしてはOSネイティブ、Flutter、React Nativeなどがありますが、開発体制や要件などにより適した技術は変わるため、これが制限されると開発やパフォーマンスなどに負の影響が出てしまいます。
ZodスキーマからSwiftコードへのジェネレーターなど、自動でネイティブアプリ向けのスキーマを生成する試みもないわけではないですが、2024年現在ライブラリとして見つかるのはこれくらいで、今後増えていくかもわかりません。
手動でスキーマから型を再構築するとなると、それこそスキーマとの乖離が問題になってきます。
バックエンド
Ruby on RailsからScalaに移行したTwitter、JavaからNodeに移行したPayPalなど、開発体制やパフォーマンスの観点からバックエンドを移行するというのは、頻繁ではありませんが選択肢には入れておきたいです。
APIスキーマが存在している場合、バックエンドアプリケーション/API/フロントエンドアプリケーションという全体のアーキテクチャが維持されており、APIが適切に設計されていれば、バックエンドをAPIに合わせて書き直すだけで段階的に移行することができます。
ところが、tRPCを採用している場合、これらのケースにおいてAPIの移行もほぼ必須になってしまい、フロントエンドのコードも書き換える必要が生じます。
フロントエンドとバックエンドが密結合になる
フロントエンドとバックエンドの間にスキーマという緩衝材がないので、一体的に開発する必要が生じます。
このため、フロントエンド担当とバックエンド担当といったように分業することが困難になります。エンジニア単位ならともかく、ベンダー単位での分業はかなり難しくなります。
使えると考えているケース
ここまでスキーマレスのデメリットがいかに致命的かということを説明してきました。
逆に言うと、ここまでのデメリットが気にならないようなケースであれば、手軽に使えるtRPCは非常に有用なツールになります。つまり、次のようなケースです。
- TypeScript以外の言語は使えなくてよい
- フロントエンドとバックエンドが密結合でよい
このことは、T3 Stackの提唱者であるTheo Browne氏の以下の図でも説明されています。
https://hackernoon.com/the-simplicity-of-trpc-with-the-power-of-graphql
では、このようなケースは具体的にはどのようなものがあるのでしょうか。
フロントエンド-BFF間通信
フロントエンドとバックエンドの間にBFFを置く場合はtRPCが有用です。BFFはフロントエンドと密結合になるのが普通なので、これが問題にはなりません。また、BFFはフロントエンドの担当が触れた方がスムーズですし、Next.jsなどのSSRフレームワークをBFFとして使用する場合、言語は要件的にTypeScript一択になります。
拡張予定のない小規模アプリケーション
フロントエンドとバックエンドをともにTypeScriptで一体開発し、参画するエンジニアも多くなく、システムの拡張性を一切考えないのであれば、tRPCを使ってもいいと思います。T3 Stackで作るのもいいですね。
ただし、技術選定をするにあたって上の前提を元にtRPCを選択し、開発速度を優先して拡張性が限られた設計をしたということをクライアント(自社開発なら企画メンバー)にきちんと説明する必要があると思います。単一機能を提供するだけと割り切るならいいですが、何年にも渡ってユーザーにサービスをあれこれ提供するようなWebアプリケーションに使うのはかなり冒険だと考えています。
まとめ
- tRPCはREST, GraphQLに比べてスキーマが不要な分手軽に利用できるが、プラットフォームの拡張・変更やエンジニアの分業が難しくなる。
- tRPCはフロントエンド-BFF間の通信や拡張予定のない小規模アプリケーションには向いているが、それ以外のケースには不向きである。