T-CREATOR

tRPC とは?型安全なフルスタック通信を実現する仕組みとメリット【2025 年版】

tRPC とは?型安全なフルスタック通信を実現する仕組みとメリット【2025 年版】

フルスタック開発で、フロントエンドとバックエンド間の型の不一致に悩まされた経験はありませんか?

API の仕様変更でフロントエンドが壊れたり、レスポンスの型チェックに時間を取られたりする課題は、多くの開発者が直面している問題です。そんな中、tRPC は型安全性を保ちながら、クライアント・サーバー間の通信を驚くほどシンプルに実装できる革新的なソリューションとして注目を集めています。

この記事では、tRPC の基本概念から、その仕組み、そして実際の開発で得られるメリットまでを、初心者の方にもわかりやすく解説していきます。

背景

Web アプリケーション開発における通信の課題

従来の Web アプリケーション開発では、フロントエンドとバックエンドが別々の言語やフレームワークで実装されることが一般的でした。

この構成では、REST API や GraphQL を介してデータをやり取りしますが、クライアント側とサーバー側で型定義を二重管理する必要があり、型の不整合が発生しやすいという問題がありました。例えば、バックエンドで API のレスポンス構造を変更した際、フロントエンド側の型定義を手動で更新し忘れると、実行時エラーが発生してしまいます。

さらに、TypeScript を使っていても、ネットワーク境界を越えるとその型安全性が失われてしまうのです。

TypeScript エコシステムの進化

TypeScript の普及により、フロントエンドだけでなくバックエンドでも TypeScript を採用するプロジェクトが増加しました。

Next.js や Remix といったフルスタックフレームワークの登場により、同一のコードベース内でクライアントとサーバーのコードを管理できるようになりました。この環境変化は、型情報を共有する新しいアプローチを可能にしたのです。

tRPC は、この TypeScript エコシステムの進化を最大限に活用し、コードベース全体で一貫した型安全性を実現するために生まれました。

以下の図は、従来の API 通信と tRPC の違いを示しています。

mermaidflowchart LR
    subgraph 従来の方式
        A1["フロントエンド<br/>(TypeScript)"] -->|"HTTP リクエスト<br/>(型情報なし)"| B1["バックエンド<br/>(TypeScript)"]
        B1 -->|"JSON レスポンス<br/>(型情報なし)"| A1
        A1 -.->|"手動で型定義"| T1["型定義ファイル"]
    end

    subgraph tRPC の方式
        A2["フロントエンド<br/>(TypeScript)"] -->|"型付きリクエスト"| B2["バックエンド<br/>(TypeScript)"]
        B2 -->|"型付きレスポンス"| A2
        B2 -.->|"自動で型推論"| A2
    end

図が示すように、従来の方式では型情報が失われる境界が存在しますが、tRPC ではエンドツーエンドで型情報が保たれます。

課題

REST API における型の問題

REST API を使った開発では、以下のような課題に直面します。

まず、型定義の二重管理が必要になります。バックエンドで定義した API の型を、フロントエンドでも別途定義しなければなりません。この作業は煩雑で、ヒューマンエラーが発生しやすくなります。

次に、実行時の型エラーのリスクがあります。API のレスポンス構造が変更されても、TypeScript のコンパイル時にはエラーが検出されず、実際にアプリケーションを実行して初めてエラーに気づくことになります。

さらに、API 仕様書のメンテナンスも大きな負担です。Swagger や OpenAPI を使って API 仕様を文書化しても、コードと仕様書の同期を保つのは困難でしょう。

GraphQL の複雑性

GraphQL は型安全性の面で REST API よりも優れていますが、別の課題があります。

学習コストの高さが挙げられます。スキーマ定義言語(SDL)を学び、リゾルバーを実装し、クエリの最適化を考慮する必要があり、小規模なプロジェクトには過剰な場合があります。

また、セットアップの複雑さも問題です。Apollo Server や GraphQL Code Generator などのツールチェーンを構築する必要があり、初期設定に時間がかかってしまいます。

N+1 問題への対処も必須です。DataLoader などの仕組みを理解し、適切に実装しないとパフォーマンスの問題が発生します。

以下の図は、各アプローチの複雑性と型安全性のトレードオフを示しています。

mermaidflowchart TD
    Start["API 設計の選択"] --> Choice{"要求される<br/>型安全性"}

    Choice -->|"不要"| REST["REST API<br/>★学習コスト: 低<br/>★型安全性: 低<br/>★セットアップ: 簡単"]
    Choice -->|"必要"| TypeChoice{"全て TypeScript?"}

    TypeChoice -->|"いいえ"| GraphQL["GraphQL<br/>★学習コスト: 高<br/>★型安全性: 中〜高<br/>★セットアップ: 複雑"]
    TypeChoice -->|"はい"| tRPC_Choice["tRPC<br/>★学習コスト: 低<br/>★型安全性: 高<br/>★セットアップ: 簡単"]

    REST --> End1["型定義の二重管理<br/>実行時エラーのリスク"]
    GraphQL --> End2["スキーマ管理<br/>リゾルバー実装<br/>N+1 問題対応"]
    tRPC_Choice --> End3["型推論による<br/>エンドツーエンド<br/>型安全性"]

この図から、tRPC が TypeScript フルスタック環境において、低い学習コストで高い型安全性を実現する選択肢であることがわかります。

解決策

tRPC の基本概念

tRPC(TypeScript Remote Procedure Call)は、TypeScript で書かれたフルスタックアプリケーションにおいて、エンドツーエンドの型安全性を実現するライブラリです。

その最大の特徴は、バックエンドで定義した手続き(プロシージャ)の型情報を、自動的にフロントエンドで利用できる点にあります。型定義ファイルを手動で書く必要はありません。

tRPC は RPC(リモートプロシージャコール)のアプローチを採用しており、サーバー側の関数をクライアントから直接呼び出すような感覚で開発できます。

内部的には HTTP を使って通信しますが、開発者はその詳細を意識する必要がありません。

tRPC が解決する主な問題

tRPC は前述の課題を以下のように解決します。

型定義の自動共有により、バックエンドで定義した型が自動的にフロントエンドに伝播されます。TypeScript の型推論を活用することで、追加のコード生成やビルドステップなしに型情報が共有されるのです。

コンパイル時の型チェックが可能になります。API の変更があった場合、フロントエンド側のコードでコンパイルエラーが発生するため、実行前に問題を発見できます。

シンプルな API 設計も実現します。GraphQL のようなスキーマ定義言語を学ぶ必要はなく、通常の TypeScript の関数を書くだけで API を作成できるでしょう。

自動的なドキュメントも生成されます。型情報そのものがドキュメントとなり、IDE の補完機能を活用できます。

以下の図は、tRPC における型情報の流れを示しています。

mermaidflowchart LR
    subgraph Server["サーバーサイド"]
        Router["tRPC Router<br/>(型定義を含む)"]
        Procedure["Procedure<br/>(関数実装)"]
        Router --> Procedure
    end

    subgraph TypeInference["型推論レイヤー"]
        AppRouter["AppRouter 型<br/>(エクスポート)"]
    end

    subgraph Client["クライアントサイド"]
        TRPCClient["tRPC Client"]
        Component["React Component"]
        TRPCClient --> Component
    end

    Router -.->|"型情報を抽出"| AppRouter
    AppRouter -.->|"型情報を提供"| TRPCClient

    Component -->|"型安全な呼び出し"| TRPCClient
    TRPCClient -->|"HTTP リクエスト"| Procedure
    Procedure -->|"型付きレスポンス"| TRPCClient

この仕組みにより、サーバー側の実装変更が即座にクライアント側の型チェックに反映されます。

tRPC のコア機能

tRPC は以下のコア機能を提供します。

Router と Procedure がその中心です。Router は複数の Procedure(API エンドポイント)をまとめたもので、Procedure には query(データ取得)と mutation(データ変更)の 2 種類があります。

Input Validation により、Zod などのバリデーションライブラリと統合し、入力データの検証を型安全に行えます。

Middleware 機能で、認証やロギングなどの共通処理を実装できます。ミドルウェアも型安全に動作します。

Subscriptions を使えば、WebSocket を利用したリアルタイム通信も型安全に実装可能です。

Batching と Caching により、複数のリクエストを自動的にまとめて送信したり、結果をキャッシュしたりすることで、パフォーマンスを最適化できます。

具体例

基本的な tRPC サーバーの実装

まず、tRPC を使った簡単なサーバーを実装してみましょう。

以下は、tRPC サーバーのセットアップ手順を示す図です。

mermaidflowchart TD
    Setup["tRPC セットアップ"] --> Init["tRPC インスタンス初期化"]
    Init --> Router["Router 作成"]
    Router --> Proc1["Query Procedure 追加"]
    Router --> Proc2["Mutation Procedure 追加"]
    Proc1 --> Export["AppRouter 型エクスポート"]
    Proc2 --> Export
    Export --> Server["HTTP サーバーに接続"]

この流れに沿って実装していきます。

必要なパッケージのインストール

まず、必要なパッケージをインストールします。

bashyarn add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

このコマンドで、tRPC のサーバー・クライアント機能、React Query との統合、入力検証用の Zod がインストールされます。

tRPC インスタンスの初期化

次に、tRPC のインスタンスを作成します。このファイルでは、プロジェクト全体で使用する tRPC の基盤を定義します。

typescript// server/trpc.ts
import { initTRPC } from '@trpc/server';

// tRPC インスタンスを初期化
// このインスタンスから router や procedure を作成します
const t = initTRPC.create();

// 外部で使用するための export
export const router = t.router;
export const publicProcedure = t.procedure;

initTRPC.create() で tRPC のコアインスタンスを作成し、そこから routerprocedure を取り出しています。

Router の定義

続いて、実際の API エンドポイント(Procedure)を含む Router を定義します。

typescript// server/routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';

// ユーザー関連の API を定義する Router
export const userRouter = router({
  // ユーザー一覧を取得する Query
  // query は読み取り専用の操作に使用します
  getUsers: publicProcedure.query(async () => {
    // 実際の実装では DB からデータを取得
    return [
      {
        id: 1,
        name: '田中太郎',
        email: 'tanaka@example.com',
      },
      {
        id: 2,
        name: '佐藤花子',
        email: 'sato@example.com',
      },
    ];
  }),
});

query は GET リクエストのような読み取り操作を定義します。戻り値の型が自動的にクライアント側に伝播されます。

入力検証を含む Mutation の実装

次に、データを変更する Mutation を実装します。Zod を使った入力検証も追加しましょう。

typescript// server/routers/user.ts(続き)
export const userRouter = router({
  // ...既存の getUsers

  // ユーザーを作成する Mutation
  // mutation はデータの作成・更新・削除に使用します
  createUser: publicProcedure
    // Zod を使って入力データのスキーマを定義
    .input(
      z.object({
        name: z.string().min(1, '名前は必須です'),
        email: z
          .string()
          .email('有効なメールアドレスを入力してください'),
      })
    )
    // input の型は自動的に推論されます
    .mutation(async ({ input }) => {
      // input は { name: string; email: string } 型
      // 実際の実装では DB にデータを保存
      const newUser = {
        id: Math.floor(Math.random() * 1000),
        name: input.name,
        email: input.email,
      };
      return newUser;
    }),
});

.input() で入力スキーマを定義すると、自動的に型推論とバリデーションが行われます。不正な入力は自動的に拒否されるのです。

ID を使った個別取得の実装

特定のユーザーを ID で取得する Procedure も追加しましょう。

typescript// server/routers/user.ts(続き)
export const userRouter = router({
  // ...既存の Procedures

  // ID でユーザーを取得する Query
  getUserById: publicProcedure
    // 数値の ID を受け取る
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      // 実際の実装では DB から検索
      const users = [
        {
          id: 1,
          name: '田中太郎',
          email: 'tanaka@example.com',
        },
        {
          id: 2,
          name: '佐藤花子',
          email: 'sato@example.com',
        },
      ];

      const user = users.find((u) => u.id === input.id);

      if (!user) {
        throw new Error('ユーザーが見つかりません');
      }

      return user;
    }),
});

エラーハンドリングも通常の TypeScript コードと同様に記述でき、エラー情報もクライアント側で型安全に受け取れます。

ルートの統合と型のエクスポート

複数の Router を統合し、アプリケーション全体の Router を作成します。

typescript// server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';

// アプリケーション全体の Router を定義
// 複数の Router を統合できます
export const appRouter = router({
  user: userRouter,
  // 他の Router もここに追加
  // post: postRouter,
  // comment: commentRouter,
});

// AppRouter の型をエクスポート
// この型がクライアント側で使用されます
export type AppRouter = typeof appRouter;

AppRouter 型をエクスポートすることで、クライアント側がこの型情報を利用できるようになります。

Next.js での HTTP ハンドラーの設定

Next.js の API Routes で tRPC を公開します。

typescript// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';

// Next.js の API ハンドラーを作成
// すべての tRPC リクエストがこのエンドポイントで処理されます
export default createNextApiHandler({
  router: appRouter,
  createContext: () => ({}), // 認証情報などを含む context を作成
});

​/​api​/​trpc​/​* へのすべてのリクエストが tRPC によって処理されます。

クライアント側の実装

次に、フロントエンドから tRPC を使用する実装を見ていきます。

以下は、クライアント側のセットアップフローです。

mermaidflowchart TD
    ClientSetup["クライアントセットアップ"] --> ImportType["AppRouter 型をインポート"]
    ImportType --> CreateClient["tRPC Client 作成"]
    CreateClient --> Provider["Provider でラップ"]
    Provider --> UseHooks["React hooks で API 呼び出し"]
    UseHooks --> TypeSafe["型安全な開発"]

この流れで実装を進めていきます。

tRPC クライアントの設定

まず、tRPC クライアントを作成します。

typescript// utils/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers/_app';

// AppRouter 型を使って tRPC React hooks を作成
// この型情報により、エンドツーエンドの型安全性が実現されます
export const trpc = createTRPCReact<AppRouter>();

AppRouter 型をジェネリクスとして渡すことで、すべての API 呼び出しが型安全になります。

Provider の設定

アプリケーション全体で tRPC を使用できるように Provider を設定します。

typescript// pages/_app.tsx
import { useState } from 'react';
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';

function MyApp({ Component, pageProps }: AppProps) {
  // React Query の QueryClient を作成
  const [queryClient] = useState(() => new QueryClient());

  // tRPC クライアントを作成
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          // tRPC サーバーの URL を指定
          url: 'http://localhost:3000/api/trpc',
          // 複数のリクエストを自動的にバッチ化
        }),
      ],
    })
  );

  return (
    <trpc.Provider
      client={trpcClient}
      queryClient={queryClient}
    >
      <QueryClientProvider client={queryClient}>
        <Component {...pageProps} />
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export default MyApp;

httpBatchLink により、複数の API 呼び出しが自動的に 1 つのリクエストにまとめられ、パフォーマンスが向上します。

React コンポーネントでの使用(Query)

実際のコンポーネントで tRPC を使用してみましょう。

typescript// components/UserList.tsx
import { trpc } from '../utils/trpc';

export function UserList() {
  // ユーザー一覧を取得
  // data の型は自動的に推論されます
  const { data, isLoading, error } =
    trpc.user.getUsers.useQuery();

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;

  return (
    <ul>
      {data?.map((user) => (
        // user.id, user.name, user.email はすべて型安全
        <li key={user.id}>
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  );
}

data の型は自動的に Array<{ id: number; name: string; email: string }> と推論され、IDE の補完が効きます。

React コンポーネントでの使用(Mutation)

データを作成する Mutation を使ってみましょう。

typescript// components/CreateUserForm.tsx
import { useState } from 'react';
import { trpc } from '../utils/trpc';

export function CreateUserForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  // Mutation を取得
  const createUser = trpc.user.createUser.useMutation();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      // mutateAsync で Mutation を実行
      // 引数の型は自動的にチェックされます
      const newUser = await createUser.mutateAsync({
        name,
        email,
      });

      console.log('作成されたユーザー:', newUser);
      // フォームをリセット
      setName('');
      setEmail('');
    } catch (error) {
      // バリデーションエラーもここでキャッチ
      console.error('エラー:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type='text'
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder='名前'
      />
      <input
        type='email'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder='メールアドレス'
      />
      <button type='submit' disabled={createUser.isLoading}>
        {createUser.isLoading
          ? '作成中...'
          : 'ユーザーを作成'}
      </button>
    </form>
  );
}

不正な型の引数を渡すと、コンパイル時にエラーが発生します。例えば、email に数値を渡そうとすると、TypeScript がエラーを出してくれるのです。

図で理解できる要点

  • tRPC は型情報をサーバーからクライアントへ自動的に伝播させる
  • Router と Procedure の構造により、API を階層的に整理できる
  • Zod による入力検証が型安全に統合される
  • React Query との統合により、データフェッチングの状態管理が簡単になる

まとめ

tRPC は、TypeScript フルスタック開発における型安全性の課題を、エレガントに解決するライブラリです。

従来の REST API や GraphQL と比較して、学習コストを抑えながら高い型安全性を実現できる点が大きな魅力でしょう。バックエンドで定義した型が自動的にフロントエンドに伝播されるため、型定義の二重管理や実行時エラーのリスクから解放されます。

特に、Next.js や Remix などのフルスタックフレームワークを使用しているプロジェクトでは、tRPC の導入により開発体験が大きく向上するはずです。

以下のような特徴があります。

項目説明
1エンドツーエンドの型安全性
2シンプルな API 設計(通常の TypeScript 関数として実装)
3自動的な型推論とバリデーション
4React Query との統合による優れた DX
5低い学習コスト

tRPC は、TypeScript で統一されたフルスタック開発において、型安全性と開発生産性を両立させる最適な選択肢の一つと言えますね。

これから tRPC を使い始める方は、まず小さなプロジェクトで基本的な Query と Mutation を試してみることをお勧めします。型推論の威力を体験すれば、その便利さに驚かれることでしょう。

関連リンク