tRPCでNext.jsのエンドツーエンド型安全性を実現する
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
現代のWeb開発の世界では、堅牢で保守性の高いアプリケーションを構築するには、フロントエンドとバックエンド間のシームレスな通信が必要です。開発者が直面する、特にTypeScriptを使用する際の永続的な課題の1つは、この境界を越えた型安全性を確保することです。従来、これには手動での型定義、コード生成ツール、または複雑なGraphQLセットアップが含まれていましたが、それぞれに独自のオーバーヘッドと複雑さが伴います。これらの方法はしばしば断絶を生み出し、フロントエンドとバックエンドの型を同期させるために追加のステップが必要になり、潜在的なバグや理想的とは言えない開発者エクスペリエンスにつながります。この記事では、革新的なフレームワークであるtRPCが、この問題に正面から取り組み、煩雑なコード生成を必要とせずに、Next.jsアプリケーションとNode.jsバックエンド間のエンドツーエンドの型安全性を実現するためのエレガントなソリューションを提供する方法を掘り下げます。
tRPCで型安全性を解き放つ
tRPCの力を最大限に理解するために、まずいくつかの基本的な概念を明確にしましょう。
主要な用語
- RPC(Remote Procedure Call、リモートプロシージャコール): ネットワーク上の別のコンピュータにあるプログラムから、ネットワークの詳細を理解することなくサービスを要求できるプロトコルです。簡単に言えば、ローカル関数のように、どこかにある関数を呼び出すことです。
- エンドツーエンドの型安全性: バックエンド(APIスキーマ、関数シグネチャなど)で定義された型が、フロントエンドで自動的に推論され、強制されることを保証し、コンパイル時に型の一致や関連するエラーを排除し、より信頼性の高いアプリケーションにつながります。
- ゼロ生成: tRPCの重要な特徴であり、中間的なクライアントサイドコードファイルを生成することなく型安全性を実現することを意味します。代わりに、バックエンドコードから直接TypeScriptの強力な推論機能を利用します。
tRPCの原則
tRPCは、シンプルでありながら深遠な原則に基づいています。それは、TypeScriptの推論エンジンを活用して、バックエンド関数定義からフロントエンドに直接型を共有することです。TypeScriptを使用してNode.jsバックエンドでAPIエンドポイント(tRPC用語で「プロシージャ」)を定義すると、tRPCはその入力引数、戻り値の型、および潜在的なエラーを推論します。この型情報はNext.jsフロントエンドに公開され、ローカル関数を呼び出すのと同じように、完全な型安全性でこれらのAPIエンドポイントを利用できます。
tRPCの実装
tRPCの仕組みを説明するために、実践的な例を見てみましょう。
バックエンドセットアップ(Node.js)
まず、tRPCとZod(スキーマ検証によく使用される)をインストールします。
npm install @trpc/server zod
次に、tRPCルーターとプロシージャを定義します。
// src/server/trpc.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); // tRPCを初期化 export const router = t.router; export const publicProcedure = t.procedure; // src/server/routers/_app.ts import { publicProcedure, router } from './trpc'; import { z } from 'zod'; const appRouter = router({ // データを取得するための「クエリ」プロシージャ getUser: publicProcedure .input(z.object({ id: z.string().uuid() })) // Zodで入力スキーマを定義 .query(async ({ input }) => { // 実際のアプリでは、データベースから取得します console.log(`Fetching user with ID: ${input.id}`); return { id: input.id, name: 'John Doe', email: 'john@example.com' }; }), // データ送信/副作用のための「ミューテーション」プロシージャ createUser: publicProcedure .input(z.object({ name: z.string(), email: z.string().email() })) .mutation(async ({ input }) => { // 実際のアプリでは、データベースに保存します console.log('Creating user:', input); return { id: 'some-generated-uuid', ...input }; }), }); export type AppRouter = typeof appRouter; // ルーター型をエクスポート
次に、Next.jsでtRPCリクエストを処理するAPIエンドポイントを設定します。これは通常、pages/api/trpc/[trpc].ts
またはNext.js App Routerのルートハンドラー内に直接行われます。
// pages/api/trpc/[trpc].ts (Pages Routerの場合) import { createNextApiHandler } from '@trpc/server/adapters/next'; import { appRouter } from '../../../server/routers/_app'; export default createNextApiHandler({ router: appRouter, createContext: () => ({ /* プロシージャのコンテキスト、例:データベース接続 */ }), });
フロントエンドセットアップ(Next.js)
tRPCクライアントライブラリとReact Query(データ取得に一般的に使用される)をインストールします。
npm install @trpc/client @tanstack/react-query @trpc/react-query
次に、tRPCクライアントとプロバイダーを作成します。
// src/utils/trpc.ts import { httpBatchLink } from '@trpc/client'; import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/routers/_app'; // バックエンドルーター型をインポート export const trpc = createTRPCReact<AppRouter>(); // 型を直接推論! // src/pages/_app.tsx (またはApp Routerのlayout.tsx) import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { trpc } from '../utils/trpc'; import type { AppProps } from 'next/app'; function MyApp({ Component, pageProps }: AppProps) { const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: '/api/trpc', // tRPC APIエンドポイント }), ], }) ); return ( <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> </trpc.Provider> ); } export default MyApp;
フロントエンドでのAPIプロシージャの利用
これで、Reactコンポーネントのどこからでも、tRPCフックを完全な型安全性で使用できます。
// src/components/UserDisplay.tsx import { trpc } from '../utils/trpc'; import React from 'react'; function UserDisplay() { // useQueryフックを使用、型はAppRouterから推論されます! const { data: user, isLoading, error } = trpc.getUser.useQuery({ id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef' }); if (isLoading) return <div>Loading user...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h2>User Details</h2> <p>ID: {user?.id}</p> <p>Name: {user?.name}</p> <p>Email: {user?.email}</p> </div> ); } function CreateUserForm() { const createUserMutation = trpc.createUser.useMutation({ onSuccess: (data) => { alert(`User created: ${data.name}`); // 必要に応じてクエリを無効化または再取得 }, onError: (err) => { alert(`Error creating user: ${err.message}`); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const name = formData.get('name') as string; const email = formData.get('email') as string; createUserMutation.mutate({ name, email }); // 入力引数は型チェックされます! }; return ( <form onSubmit={handleSubmit}> <h3>Create New User</h3> <input name="name" type="text" placeholder="Name" required /> <input name="email" type="email" placeholder="Email" required /> <button type="submit" disabled={createUserMutation.isLoading}> {createUserMutation.isLoading ? 'Creating...' : 'Create User'} </button> </form> ); } export default function Home() { return ( <div> <UserDisplay /> <CreateUserForm /> </div> ) }
trpc.getUser.useQuery
は、そのinput
がid
がstring
型のオブジェクトであることを自動的に認識し、そのdata
がバックエンドの{ id: string, name: string, email: string }
形状に準拠することに注目してください。同様に、createUserMutation.mutate
は、バックエンドのcreateUser
プロシージャで定義されているように、name
とemail
を期待します。間違った型を渡そうとしたり、必須フィールドを省略したりすると、コードが実行されるずっと前に、TypeScriptがすぐにコンパイル時エラーを指摘します。これが、ゼロ生成エンドツーエンド型安全性の魔法です!
アプリケーションシナリオ
tRPCは特に以下に適しています:
- モノリシックまたは密接に結合されたフルスタックアプリケーション: フロントエンドとバックエンドが同じチームまたは同じリポジトリ内で開発され、直接の型共有が効率的である場合。
- 内部ツール/管理パネル: 迅速な開発と強力な型保証が、内部運用の間違いを防ぐために不可欠な場合。
- 開発者エクスペリエンスを優先するプロジェクト: tRPCは、コンテキストスイッチングや手動での型同期の労力を大幅に削減します。
- 共有型を持つマイクロサービス: tRPCはモノリスで際立っていますが、サービスが共有型定義で開発されている場合、マイクロサービスアーキテクチャでもメリットを提供できます。
結論
tRPCは、Next.jsとNode.jsバックエンド間の型安全性ギャップを埋めるための非常に効果的なソリューションとして際立っています。TypeScriptの推論機能をインテリジェントに活用することで、手動での型定義や複雑なコード生成の必要性を排除し、真にシームレスで満足のいく開発者エクスペリエンスを提供します。tRPCを使用すると、フロントエンドとバックエンドが常に完璧な型調和で通信していることを知って、自信を持ってフルスタックアプリケーションを構築できます。これにより、開発者はより速く、堅牢で、エラーのないアプリケーションをシップできるようになり、最新のWeb開発エコシステムにおける強力なツールとしての地位を確固たるものにします。