7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

tRPCを使ってみたので、基本的な使い方・他の通信方法との比較した上でメリットを整理してみた!

Last updated at Posted at 2025-12-15

はじめに

この投稿はアイスタイルAdvent Calendar 2025の16日目の記事です。
アイスタイルでは主にバックエンド領域でアットコスメのメディア向けの機能を開発・運用をしているuchimuramです!
現在、アットコスメの検索機能の一部でtRPCを用いて開発を実施したので、いままでRESTやGraphQLくらいしか触ってことなかったので、他の通信方法を比較したうえでtRPCの特徴を紹介したいと思います!

記事の対象読者と学べる内容について

本記事では、主にバックエンド向けのAPI開発やフロントエンドとの通信で、RESTは利用してきたがほかのクエリ言語を触ったことがない方やそもそもtRPCを知らない人に向けた内容となります。また、tRPCの名前は聞いたことがあるけど使ったことがない方もぜひ読んでいただけると幸いです。

tRPCとは

tRPCとは、TypeScript向けのRPCフレームワークで、TypeScriptの型システムを活用することで、クライアント側とサーバー間の通信を型で表現し、型安全や関数呼び出しとして扱うことができるライブラリです。

主な特徴

tRPCの特徴として

  • Zodなどのバリデーションライブラリを使ってスキーマを構築することができる
  • モノレポ構成にすることでクライアント側とサーバーサイドで型定義を1箇所に集約することができる
  • TypeScriptのみで利用できる

が挙げられます。

tRPCでは、RESTのようなリソース指向ではなくプロシージャとしてAPIを定義します。
プロシージャとは、tRPCにおけるAPIの最小単位のことを指します。
tRPCのプロシージャは主に3種類に別れます。

1つ目のqueryは、データ取得の用途で利用します。
2つ目のmutationは、データ変更(新規作成・変更・削除)の用途で利用します。
3つ目のsubscriptionは、クライアント・サーバーサイド間のリアルタイム通信の用途で利用します。

種類 役割 RESTでのイメージ
query データ取得 GET
mutation データ変更 POST・PUT・PATCH・DELETE
subscription リアルタイム通信 WebSocket

基本的な使い方

実際にtRPCを利用するときの一例としてどのように構築すればいいかサンプルコードで用いて説明したいと思います。今回はサンプルコードでよくある、フロントエンドをNext.js・サーバーサイドをExpressを用いたコードになります。

  • モノレポ構成のディレクトリ
    tRPCで利用するとき、型安全にスキーマを利用するためにモノレポ構成で実施します。
src/
├─ shared/          ← 共通スキーマ
│  └─ user.schema.ts
├─ server/          ← バックエンド
│  ├─ trpc.ts
│  └─ router.ts
└─ client/          ← フロントエンド
   └─ trpc.ts
  • 共通スキーマをZodを用いて作成する
    共通スキーマはバリデーションライブラリを用いて実装します。今回はZodを用いてI/Fを定義します。(もちろん、Zod以外のライブラリを利用することも可能です)
// 入力・出力の型定義を1箇所に集約
// shared/user.schema.ts
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
});

export type User = z.infer<typeof UserSchema>;

export const CreateUserInputSchema = z.object({
  name: z.string().min(1),
});

export type CreateUserInput = z.infer<typeof CreateUserInputSchema>;

クライアント側について(Next.jsを用いた参考例)

tRPCの設定を記述して、query・mutationを呼び出すことで、従来と同様にデータ取得やデータ作成/更新/削除ができます。

  • 初期化
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCReact<AppRouter>();

  • Query(取得)
// pages/index.tsx
import { trpc } from '../client/trpc';

export default function Home() {
  const { data, isLoading } = trpc.userList.useQuery();

  if (isLoading) return <p>Loading...</p>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

  • Mutation(更新)
const CreateUser = () => {
  const mutation = trpc.createUser.useMutation();

  return (
    <button
      onClick={() => mutation.mutate({ name: 'Charlie' })}
    >
      Create User
    </button>
  );
};

サーバーサイド側について

初期化の設定をしたあと、router.tsで各エンドポイントの宣言を作成します。(controllerと同様の役割をしています。)

  • tRPC初期化
// server/trpc.ts
import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

export const router = t.router;
export const procedure = t.procedure;


// Expressを利用したサンプル
// server/index.ts
import express from 'express';
import * as trpcExpress from '@trpc/server/adapters/express';
import { appRouter } from './router';

const app = express();

app.use(
  '/trpc',
  trpcExpress.createExpressMiddleware({
    router: appRouter,
  }),
);

app.listen(3000);
  • Router/Procedure定義
// server/router.ts
import { router, procedure } from './trpc';
import {
  UserSchema,
  CreateUserInputSchema,
} from '../shared/user.schema';

export const appRouter = router({
  // Query部分
  userList: procedure.query(() => {
    return [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
  }),

  // Mutation部分
  createUser: procedure
    .input(CreateUserInputSchema)
    .mutation(({ input }) => {
      return {
        id: Date.now(),
        name: input.name,
      };
    }),
});

// 👇 フロントで使う型
export type AppRouter = typeof appRouter;

REST API・GraphQL・gRPCとの違い

tRPCで実装方法をまとめましたが、そもそもtRPC以外のREST API・GraphQL・gRPCの違いが気になると思います。それぞれの違い表でまとめると

tRPC REST GraphQL gRPC
メリット 開発体験がいい
型定義の2重管理が不要
関数の呼び出し感覚で利用可能
HTTP標準で理解しやすい
エコシステムが成熟している
クライアント側が必要なデータを選択できる
型システムが強力
HTTPSを利用するので高速
強い型安全
デメリット TypeScript専用なので、サーバーサイドの言語もTypeScript向けのFWを利用する必要がある クライアント側とサーバーサイド側で型ズレが起きやすい スキーマ設計が重い
N + 1問題が起きる可能性があるので設計難易度が高い
バイナリーファイルを利用するので直接触りにくい

ブラウザとの相性が悪い
スキーマ作成 TypeScriptの型で構築 OpenAPIを使ってまとめる GraphQL Schemaを必須で定義する必要がある .protoを必須で定義
スキーマ管理 Zodなどのバリデーションライブラリを利用する OpenAPI / DTO / FE型など スキーマを起点に実装・型・クエリを管理 .protoで管理する
対応言語 TypeScriptのみ 多言語サポート 多言語サポート 多言語サポート
学習コスト 低い 低い 高め 高め

になります。
特に他のAPI通信方法と比較したときにサーバーサイド側もTypeScriptで作成しないとtRPCを活用することができないという制約があるものの、GraphQLやgRPCのような型安全の特徴をもちつつ、RESTを一度でも触ったことがある方なら学習コストは比較的低いと感じます。

個人的に感じたtRPCのメリットについて

1. 共通スキーマを同一言語で実施しているので、型の恩恵を受けることができる

クライアント側とサーバーサイド側をともにTypeScriptで実装している恩恵があり、クライアント↔サーバーサイド間で型変換がスムーズに行くことが多いと感じました。とくに、スキーマに時刻データを入れるとき、クライアント・サーバーサイドともにDate型を利用できるため、RESTやGraphQLで発生していたパース処理をせずとも利用できる部分はとても便利でした。
※Date型をそのまま利用するためにsuperjsonなどtransformerを有効にする必要があります。

  • クライアント
// client/trpcClient.ts
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
import { trpc } from './trpc';

export const trpcClient = trpc.createClient({
  // trpcの設定にsuperjsonの記述を入れる
  transformer: superjson,
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});
  • サーバーサイド
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

export const t = initTRPC.create({
  // trpcの設定にsuperjsonの記述を入れる
  transformer: superjson,
});

export const router = t.router;
export const procedure = t.procedure;

// server/router.ts
import { router, procedure } from './trpc';
import { z } from 'zod';

export const appRouter = router({
  getUser: procedure.query(() => {
    return {
      id: 1,
      name: 'Alice',
      createdAt: new Date(), // ← Date型
    };
  }),
});

export type AppRouter = typeof appRouter;

2.開発体験が良い

tRPCで連携するときにクライアント側とサーバーサイド側ともにTypeScriptで実装するので、複数言語を利用しないので実装者のスイッチングコストが低く、よりスムーズに実装することができます。特に小規模のチームやクライアント側とサーバーサイド側が綿密に関わるBFFサーバーの用途にも向いていると感じました。

3. AI駆動開発との相性がいい

実装時やデバック時にGitHub CopilotなどのAI Agentを使って開発をするときモノレポ構成にするとAI Agentとの相性が良いと感じました。
従来なら、クライアント側とサーバー間でリポジトリが別れている場合、AIにコンテキストを渡す手間がかかりましたが、tRPCの場合、モノレポ構成にすると共通スキーマやクライアント・サーバーサイド間のファイルがコンテキストとしてのヒントとなり原因・改善のスピードが向上したように感じました。
また、共通スキーマである型定義ファイルに説明文を追加したり、各プロパティのデフォルト値や説明文を追記することで、I/Fの仕様や条件を入れることができるので、専用のマークダウンファイルを準備しなくても精度高く回答していることが多くなりました。

  • 共通スキーマにコンテキストを含めることでAIの回答精度を高めることができる
// shared/user.schema.ts
import { z } from 'zod';

export const CreateUserSchema = z.object({
  name: z
    .string()
    .min(1)
    .describe('ユーザーの表示名。画面上や通知に使用される'),

  role: z
    .enum(['admin', 'user'])
    .default('user')
    .describe('ユーザー権限。通常は user、管理操作が可能な場合は admin'),

  createdAt: z
    .date()
    .default(() => new Date())
    .describe('ユーザー作成日時。サーバー側で自動付与される'),
});

まとめ

本記事では、tRPCの基本的な概念から実装例、REST API・GraphQL・gRPCとの違い、そして実際に使って感じたメリットについて整理しました。
tRPCは、

  • クライアント・サーバー間の通信を 関数呼び出しとして扱える

  • 型定義を1箇所に集約でき、型ズレを防げる

  • superjsonなどを使うことで Date型のような非JSON型も自然に扱える

  • モノレポ構成と相性が良く、AI駆動開発のコンテキスト共有にも強い

といった特徴を持ち、特にTypeScriptを中心とした開発環境 や BFF用途 では非常に高い開発体験を得られると感じました。

一方で、TypeScript専用である点や、モノレポ構成が前提になりやすい点など、プロジェクトの規模やチーム構成によっては向き・不向きがあるのも事実なので

もし

  • TypeScript中心の開発をしている
  • フロントエンドとバックエンドの連携コストを下げたい
  • 型安全を活かした開発体験を重視したい

という用途において選択肢の1つとして有効であると感じました。
本日の記事は以上になります。
引き続き、アイスタイルAdvent Calendar 2025をお楽しみください!

参考文献

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?