はじめに
この投稿はアイスタイル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をお楽しみください!
参考文献