tRPCによるフルスタックTypeScriptでのエンドツーエンドの型安全性の実現
Grace Collins
Solutions Engineer · Leapcell

はじめに
今日のペースの速い開発環境において、堅牢で保守性の高いアプリケーションを構築することは極めて重要です。JavaScriptエコシステムが進化するにつれて、TypeScriptは、特に大規模プロジェクトにおいて、コード品質と開発者エクスペリエンスを向上させるための基盤として浮上してきました。しかし、フルスタックTypeScriptアプリケーションにおける長年の課題は、スタック全体で厳密な型安全性を維持することでした。通常、開発者はバックエンドとフロントエンド間でAPI型を手動で作成および同期することに頼っていますが、これはエラーを起こしやすく、保守が面倒で、かなりの摩擦の原因となります。これにより、開発サイクルの後半、あるいはさらに悪いことに、本番環境でしか発見されない実行時型の不一致が生じることがよくあります。
フロントエンドが、追加の構成や重複する型定義なしに、バックエンドAPIが期待および返すデータの正確な型を魔法のように知っている世界を想像してみてください。これはまさにtRPCが目指すニルヴァーナです。tRPCは、API通信への革新的なアプローチをもたらすことで、開発者が真のエンドツーエンドの型安全性を達成できるようにし、開発を合理化し、バグを減らし、全体的な開発者エクスペリエンスを大幅に向上させます。この記事では、tRPCがこの顕著な偉業をどのように達成するかを掘り下げ、そのコアコンセプトを説明し、実践的なコード例で実装を示し、フルスタックTypeScript開発への変革的な影響を強調します。
tRPCのコアコンセプト
実装の詳細に入る前に、tRPCの力を支える基本的な概念を明確に理解しましょう。
-
tRPC (TypeScript Remote Procedure Call): tRPCは、コード生成やスキーマ定義を必要とせずに、完全に型安全なAPIを構築できるフレームワークです。これにより、フロントエンドはバックエンドで定義された関数を、型整合性を維持しながら、まるでローカル関数であるかのように直接呼び出すことができます。これは、スキーマや手動の型定義のような中間レイヤーが必要な従来のRESTまたはGraphQL APIからの重要な逸脱です。
-
プロシージャ: tRPCでは、バックエンドAPIの機能は「プロシージャ」として公開されます。これらは本質的にサーバー上に存在する関数であり、クライアントによって呼び出すことができます。プロシージャは、データの取得(クエリ)、データの変更(ミューテーション)、リアルタイム更新(サブスクリプション)にすることができます。
-
ルーター: プロシージャはルーターに編成されます。ルーターは関連するプロシージャのコレクションであり、構造化されモジュール化されたAPI設計を可能にします。ルーターをネストして、より複雑なAPI階層を作成することもできます。
-
コードからの推論: tRPCの魔法は、バックエンドコードから直接型を推論する能力にあります。型を別個に定義することを強制するのではなく、tRPCはTypeScriptの強力な推論エンジンを活用して、サーバーサイドのプロシージャ定義に基づいて、クライアントに必要な型を自動的に作成します。これにより、手動の型同期が不要になります。
-
ミニマリズム: tRPCは、軽量でインフラストラクチャに関する意見を表明しないことに誇りを持っています。データベースやフロントエンドフレームワークを指示するのではなく、既存のプロジェクトに統合するための柔軟性を提供します。
tRPC によるエンドツーエンドの型安全性実装
tRPCがエンドツーエンドの型安全性をどのように実現するかを説明するために、実践的な例をステップバイステップで見てみましょう。tRPCバックエンドとReactフロントエンドを備えたシンプルなフルスタックアプリケーションをセットアップします。
1. バックエンドのセットアップ
まず、TypeScriptでNode.jsプロジェクトを初期化し、必要なtRPCパッケージをインストールする必要があります。
mkdir trpc-example cd trpc-example npm init -y npm i express @trpc/server zod @trpc/client @trpc/react-query @tanstack/react-query npm i -D typescript ts-node @types/node @types/express npx tsc --init
次に、バックエンドサーバーを作成します。ユーザーリストを取得するシンプルなプロシージャを定義します。
src/server/index.ts
import { inferAsyncReturnType, initTRPC } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; import express, { Express } from 'express'; import { z } from 'zod'; // スキーマ検証のためのZod // データベースのシミュレーション const users = [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, ]; // tRPCの初期化 const t = initTRPC.create(); // ルーターの定義 const appRouter = t.router({ user: t.router({ getUsers: t.procedure.query(() => { return users; }), getUserById: t.procedure .input(z.object({ id: z.string() })) // Zodを使用して入力スキーマを定義 .query(({ input }) => { return users.find((user) => user.id === input.id); }), createUser: t.procedure .input(z.object({ name: z.string().min(3) })) // ミューテーションのための入力スキーマを定義 .mutation(({ input }) => { const newUser = { id: String(users.length + 1), name: input.name }; users.push(newUser); return newUser; }), }), }); // 型安全なルーターのエクスポート export type AppRouter = typeof appRouter; const app: Express = express(); const port = 3000; app.use( '/trpc', trpcExpress.createExpressMiddleware({ router: appRouter, createContext: ({ req, res }) => ({}), // とりあえず基本的なコンテキスト }) ); app.listen(port, () => { console.log(`Server listening on port ${port}`); });
説明:
initTRPC.create()
を使用してtRPCを初期化します。- ネストされた
user
ルーターを含むappRouter
を定義します。 getUsers
は、すべてのユーザーを返すquery
プロシージャです。ここでは明示的な戻り値の型は注釈付けられていないことに注意してください。tRPCがそれを推論します。getUserById
は、id
を入力として取る別のquery
です。入力のスキーマを定義するためにzod
(z
) を使用し、型安全性と実行時検証を保証します。createUser
はmutation
プロシージャで、データの作成、更新、削除方法を示しています。入力検証にもZodを使用します。- 最も重要なのは、
export type AppRouter = typeof appRouter;
をエクスポートすることです。この行はtRPCの型推論の基盤です。フロントエンドがこの型をインポートすると、利用可能なすべてのプロシージャ、その入力、およびその出力に関する完全な知識を得ることができます。
2. フロントエンドのセットアップ
次に、tRPC APIを利用するシンプルなReactフロントエンドを作成しましょう。
src/client/main.tsx
(迅速なReactセットアップのためのViteを使用)
import React from 'react'; import ReactDOM from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import { trpc } from './trpc'; // 必要なtRPCクライアントインスタンス import App from './App'; const queryClient = new QueryClient(); // tRPCクライアントインスタンスの作成 const trpcClient = trpc.createClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', // tRPCサーバーのURL }), ], }); ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </trpc.Provider> </React.StrictMode> );
src/client/trpc.ts
(このファイルがtRPCクライアントをブートストラップします)
import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/index'; // バックエンドからAppRouter型をインポート export const trpc = createTRPCReact<AppRouter>();
説明:
src/client/trpc.ts
で、``../server/indexから
AppRouterを直接インポートします。これが魔法の源です!
createTRPCReact<AppRouter>()は、このインポートされた型を使用して、完全に型安全な
trpc` クライアントインスタンスを作成します。trpc
クライアントは、trpc.Provider
を使用してReactアプリケーションに提供されます。
src/client/App.tsx
import React, { useState } from 'react'; import { trpc } from './trpc'; function App() { const [newUserName, setNewUserName] = useState(''); const [userIdInput, setUserIdInput] = useState(''); // 型安全なtrpcクライアントを使用してバックエンドプロシージャを呼び出す const { data: users, isLoading: isLoadingUsers, refetch: refetchUsers } = trpc.user.getUsers.useQuery(); const { data: userById, isLoading: isLoadingUserById } = trpc.user.getUserById.useQuery( { id: userIdInput }, { enabled: !!userIdInput } // userIdInputが存在する場合のみクエリを実行 ); const createUserMutation = trpc.user.createUser.useMutation({ onSuccess: () => { refetchUsers(); // 新しいユーザー作成後にユーザーを再取得 setNewUserName(''); }, }); const handleCreateUser = () => { if (newUserName.trim()) { createUserMutation.mutate({ name: newUserName }); // 型安全な入力! } }; return ( <div> <h1>tRPC フルスタック例</h1> <section> <h2>ユーザー</h2> {isLoadingUsers && <p>ユーザーを読み込み中...</p>} <ul> {users?.map((user) => ( <li key={user.id}> {user.id}: {user.name} </li> ))} </ul> <h3>新規ユーザー作成</h3> <input type=