スキーマやコード生成なしでタイプセーフなAPIを構築・利用できる tRPC
(trpc.io)- tRPCはフルスタックのTypeScriptアプリで TypeScript推論 を共有し、サーバーとクライアント間のAPIの型不一致を減らすためのツール
- サーバーAPIを変更すると、クライアントコード側で即座に TypeScriptエラーと自動補完 によって影響を確認でき、リファクタリングの負担を減らせる
- 別個のスキーマや コード生成 の工程なしで動作し、ビルド手順やランタイム負荷を増やさずにタイプセーフなAPIを構成できる
- React、Next.js、Express、Fastify、AWS Lambda、Solid、Svelte など、さまざまな JavaScript環境 に組み込めるアダプターを提供する
- 基本的な流れは、プロシージャの定義、HTTPサーバーの作成、
AppRouter型を渡したクライアント接続であり、APIサーバーコードをSDKのように使う開発体験を目指している
tRPCが変えるAPI開発のやり方
- tRPCはフルスタックアプリケーションで、サーバーとクライアントが同じ TypeScriptの型情報 を活用できるようにし、生産性を高める
- 中核にあるのは、エンドツーエンドでタイプセーフなAPIを簡単に作り、従来のAPIレイヤーに伴う繰り返し作業を減らすこと
- サーバー側の変更がクライアントの利用箇所に影響すると、TypeScriptがエラーを通知するため、クライアント・サーバー境界の型不一致をすばやく確認できる
開発者が実感する主な特徴
-
自動的な型安全性
- サーバー側の変更がクライアントコードに影響すると、TypeScriptエラーとして表れる
-
迅速な開発者体験
- tRPCにはビルドやコンパイルのステップがない
- コード生成、ランタイム負荷、追加のビルド手順なしで使える
-
フレームワーク非依存
- JavaScriptフレームワークやランタイム全般で利用できる
- 既存プロジェクトにも簡単に追加できる
-
自動補完
- APIサーバーコードをSDKのように扱える体験を提供する
- エンドポイント利用時に型ベースのヒントを得られる
-
小さなクライアントフットプリント
- tRPCは依存関係がなく、クライアント側のサイズが小さい
-
同梱アダプター
- React、Next.js、Express、Fastify、AWS Lambda、Solid、Svelte など向けのアダプターを提供する
基本的な利用フロー
- tRPC APIはまず プロシージャ(procedure) を定義するところから始まる
- プロシージャはバックエンドを構成する関数で、組み合わせ可能であり、クエリ、ミューテーション、サブスクリプションとして作成できる
- 複数のプロシージャはルーターにまとめられる
-
プロシージャの定義
- 例では
initTRPC.create()でtRPCインスタンスを作成し、routerとpublicProcedureを構成する greetingプロシージャはname文字列を入力として受け取り、Hello ${input.name}形式の文字列を返す- 入力検証には Zod を使い、クライアント入力がプロシージャの期待する形式と正確に一致するようにする
- ファイル末尾で
export type AppRouter = typeof appRouter;によりルーター型をエクスポートし、フロントエンドコードで使えるようにする
- 例では
-
HTTPサーバーの作成
createHTTPServerにappRouterを渡してtRPCサーバーを実行する- 例ではAPIがポート
3000でリッスンするようlisten(3000)を呼び出す - tRPCは Next.js、Express、Fetch APIベースの環境、Fastify、AWS Lambda、vanilla Node HTTPサーバー向けのアダプターを提供する
- Fetch APIベースの環境の例として、Astro、Remix、SvelteKit、Cloudflare Workers が含まれる
-
クライアント接続とクエリ実行
- サーバーが起動したら、
createTRPCClient<AppRouter>でクライアントを作成し、データをクエリできる - 例のクライアントは
httpBatchLinkを使ってhttp://localhost:3000に接続する trpc.greeting.query({ name: 'John' })のように呼び出して、サーバーのgreetingプロシージャを利用できる- クライアント作成時に
AppRouter型を渡すと、バックエンドAPIと一致した TypeScriptの自動補完 と IntelliSense をコード生成なしで得られる
- サーバーが起動したら、
スタート用テンプレートと制作意図
- すぐに試せるテンプレートとして Use this template が提供されている
- tRPCの作者は、従来のAPIレイヤーの必要性を減らし、素早く反復開発しながらもアプリが壊れないという確信を与えるためにtRPCを作った
- tRPCは先進的な技術チームや複数のFortune 500企業で使われている
開発者の反応
- 複数の開発者コメントでは、tRPCはコード品質、提供スピード、開発者満足度、クライアント・サーバー境界のリファクタリング、入力検証、型付きミドルウェア体験を改善するツールとして評価されている
- TypeScriptモノレポでは、素のRESTやGraphQLよりもシンプルでありながら強い型を提供する選択肢として受け止められている
- Stripe APIのペイロードをサーバーから返しても、Reactコンポーネント側でレスポンスデータの型を受け取れるユースケースが紹介されている
1件のコメント
Hacker Newsの意見
いまのコードベースから tRPCの削除を進めているところだが、強い結合のせいで悪夢のようだった
ジュニア開発者がインターフェースやデータアクセスパターンを考えないように誘導してしまう面もあり、Prismaからコンポーネントまでそのままつながるマッピングができてしまった
迅速なプロトタイピングには素晴らしいが、後でコードベースを分離しようとすると、すぐに行き止まりに閉じ込められる
TypeScript以外の言語にも拡張できるので、バックエンドを別の言語へ移したり、SwiftやKotlinのようなネイティブモバイルクライアントを作ったりするときにも役立つ
tRPCは素晴らしいが、結局はバックエンドとフロントエンドの間のトランスポート層にすぎない
tRPCの内部構造をビジネスロジックの奥深くで使わせるのは、入力、スキーマ、分離を明確に定義するコントローラーやルーター層がないのと同じくらい悪い
だから後でtRPCから離れるとしても比較的簡単そうだし、たとえばサブシステム全体をキュー上で動くように移すのも難しくなさそうだ
こういうものには学習曲線があり、たいていは
type FunctionIWroteTodayArgs = …のような、役に立たず何も説明しない型から始まる何度か反復するうちに、目的はコードを複製することではなく、ドメインを説明し、再利用可能で有益な型、インターフェース、APIを作ることだと徐々に気づく
だからtRPCを取り除くより、チームと一緒にその部分を改善するのが正しいと思う
サーバー側でHTTPクライアントを宣言し、クライアント側でそれを利用することが、なぜより悪いのかよく分からない
私たちはサービスを作り、WebインターフェースやCLIのようなすべての利用者が使うパターンを採っているが、こうしたものが壊れないという点は、以前に見たどの方式よりも大きな改善だった
ジュニア開発者がインターフェースを考えないという不満は、tRPCに限らずどんなAPIを使っても同じように出る可能性が高い
NotionではtRPCに似たAPIスタイルを使ってきており、そのAPIはtRPCより4年ほど前から存在していた
TypeScriptのマップ型を使えば、こういうものは簡単に自作できる。キーをAPI名、値を
{ request, response }型にしたオブジェクト型を作ればよいサーバー側は各APIハンドラーを
APIs["addUser"]["request"]を受け取りPromiseを返す関数として定義し、クライアント側は同じ引数と戻り値の型を持つ非同期関数として公開すればよいこの戦略を、HTTPSの内部API、WebSocketベースのリアルタイムAPI、ElectronとWebview間のIPC、iOS/AndroidネイティブとWebview間のOS WebView IPCに使っている
ネイティブAPIではサーバー側がSwiftやKotlinなので、リクエスト/レスポンス型をTypeScriptで手書きし直している。いつかは独自IDLを持つバイナリ形式に変えるだろうが、ゆっくり大きくなる単一のクロス言語APIには、Protobufのようなものの開発者体験上のコストはまだ見合うようには見えなかった
完璧ではないが、リクエスト/レスポンス型付きのかなり良いAPIインターフェースを提供してくれる
さらに
mockApi((request) => response)のように呼び出せる10行ほどのモックAPIラッパーを作った。モック関数がAPIを正しく実装しているかを型チェックし、実際のAPI関数とまったく同じ見た目の関数を返す[0]: https://github.com/ferdikoomen/openapi-typescript-codegen
こうしたアプローチを採用した、よく管理され宣伝もされているライブラリがまだ出ていないので、あまり議論されていないのだと思う
tRPC、JSON Schemaと型生成、Zodのようなものを使うときに重要な価値はまさにその部分だ
呼び出し時にジェネリック引数を自分で渡せとまで言っているが、依存関係ツリーを整理された状態に保とうとするとかなり微妙だ
https://github.com/mikew/transmission-material-ui/blob/maste...
https://github.com/mikew/transmission-material-ui/blob/maste...
tRPC がとても好き。TypeScript 専用スタックで開発者体験を限界まで押し上げた点は驚くべきもので、GraphQL コミュニティにクエリ言語の限界とトレードオフを認識させた
同時に tRPC は本当に速く流行のサイクルを通り過ぎて、REST や GraphQL から RPC へ大規模な移行が起きているようには見えない
ただ、最近は RPC への関心は高そうで、私たちが作った BFF フレームワーク(https://wundergraph.com/)でも tRPC と以前の NextJS のアイデアを一部採用し、ファイルベースルーティングと RPC を組み合わせた
tRPC に加えて各オペレーションごとに JSON Schema を自動生成し、オペレーション全体の集合に対する OpenAPI 仕様も生成する
RPC エンドポイントのまとまりを OpenAPI 仕様や Postman コレクションとして簡単に共有できるので、このアプローチは好まれている。HTTP メソッドについての議論もほとんどなく、実質的にはクエリ、変更、サブスクリプションだけがある
最近 GraphQL、REST、RPC スタイルの API をどう使っているのか、何人・何チームが API に関わっているのか気になる
REST API 側には ts-rest(https://ts-rest.com)、zodios(https://www.zodios.org)、Hono(https://hono.dev)がある
チームで複数の言語を使っているなら Fern もある: https://www.buildwithfern.com
私たちは連合スキーマが変わるたびに GraphQL 型を生成し、ファイルを保存するたびにクエリと変更のレスポンス型を生成している
エンドポイントのコンシューマ生成ライブラリで簡単に抽象化することもできるが、それ以外に一般的な Web API と比べて実際に何が得られるのか分からない
いずれにせよ各バックエンド呼び出しでやるべきことはそのまま実行しなければならない
tRPC が好き。これまで見たフルスタック開発者体験の中で断然最高で、特に Zod と一緒に使うと API が本当に素晴らしい
Zod と tRPC は TypeScript の未来における重要なプロジェクトだと見ていて、今後数年で TypeScript エコシステム全体に tRPC に着想を得た開発者体験が大きく花開くと思う
すでに tRPC の DNA がはっきりしているプロジェクトとしては、別のユースケースを狙う Ping の UploadThing(https://github.com/pingdotgg/uploadthing)と、私たちの Lusat(https://github.com/lusatai/lusat)がある
また、検証エラーの型がどのスキーマを検査するかによって変わる点も面倒。エラー処理が予測不能になり、例外的なケースが常に多すぎる
改善の余地が大きい
ユニットテストを書くのもずっと楽になった
大きな問題は型安全な CRUDとデータマイグレーションだと思うが、Mongoose はよく言及されるものの、型安全性の面では Zod/TypeScript より大きく後退する感じがする
今は少し大げさに言いたい気分だが、特定のプログラミングスタイルでは、コードベースに Zod を追加することは TypeScript を追加するのと同じくらい大きな利点がある、とまで言える
スキーマから始めて型付きサービスを作るパターンは、関数型プログラミング寄りの人たちによく合う
tRPC がバージョン差とマイグレーションをどう扱うのか気になる
フィールドにはライフサイクルがある。最初に導入されるときは、どのクライアントもサーバーもそのフィールドを知らない
クライアントとサーバーは一度にすべて再起動されるわけではなく、同じバージョンの型を持っているわけでもない
データはあるバージョンの型で書かれ、別のバージョンの型で読まれることになる
すべてのバイナリ、実行中のプログラム、データがアップグレードされると保証でき、永続データがないなら問題ではないかもしれない
しかし複数の組織が絡む瞬間、すべてのアプリとサーバーが最新のライブラリバージョンに同期され、再ビルドされ、再デプロイされると保証するのは難しい
静的型検査はバージョン差がないと仮定する。バイナリ内のすべてのコードは、型を定義する同じバージョンのライブラリでビルドされた閉じた世界である
Zod は未知のフィールドを許可でき、その後オプションフィールドへ移行し、状況が安定したら必須フィールドにできる
もちろん、tRPC にやってほしいと最も望む層ほど、こういう種類の問題を考える可能性は最も低い
1つの言語だけを対象にするなら、スキーマやコード生成が不要なのは当然
tRPC を約5万行の Web アプリケーション2つで使っていて、とても気に入っている。開発者体験が素晴らしい
ただし tRPC の流行期はすでにしばらく前に過ぎており、RSC の流行がすぐに追いついた感じがする
最近は tRPC のように安定していて良い解決策があるのに、みんな RSC を使うかどうかの話ばかりしている
RSC に反対しているわけではないが、議論が多すぎる。最近アプリケーションを作るとき、tRPC はとても実用的な方法だ
追記: RSC は React Server Components のことで、独自の読み書きデータ哲学も伴っている
人々が知らないかもしれない略語は、定義するかリンクを付けてほしい
いくつかの個人プロジェクトで tRPC と Next.js を使ってみたが、良い体験だった
特に Create T3 App(https://create.t3.gg/) のような事前設定済みテンプレートと一緒に使うと、イテレーション速度ではなかなか勝てない
一緒に働いていた開発者が tRPC を絶賛するまでは知らなかったが、知ってからはあまりにも明らかに良いものに見えた
一緒に T3 アプリ(tRPC、Next.js、Tailwind、TypeScript、Prisma)を作った。見たいならここにある: https://github.com/stytchauth/stytch-t3-example
TypeScript で作業するとき、型安全な API は本当に大きな助けになる