tRPCは、TypeScriptで書いたサーバーの関数を、クライアントから型ごと呼び出せるようにするRPCライブラリです。スキーマ定義もコード生成も挟まず、サーバー側の型がそのままクライアントの補完と型チェックに効きます。この記事では、REST・GraphQL・gRPCとの違いを整理したうえで、2026年10月28日(UTC)公開の最新版11.22.1を手元で動かし、HTTPで実際に何が流れるか、エラーがどう返るか、Date型がどう崩れるかを実測した結果とコードで示します。
まとめ:tRPCを選ぶ条件と11.22時点の要点
- tRPCとは:サーバーのルーターの型を、クライアントが
import typeで受け取って呼び出す仕組み。.protoやGraphQL SDLのような別言語のスキーマを持たない。 - 選ぶ条件:フロントとバックエンドがどちらもTypeScriptで、サーバーの型をクライアントが常に最新の状態で参照できること。モノレポが最も簡単で、別リポジトリなら型をnpmパッケージで配る。
- REST・GraphQL・gRPCとの違い:3方式は言語に依存しない契約(OpenAPI・GraphQLスキーマ・.proto)を持てるのに対し、tRPCはTypeScriptの型推論で直接つなぐ。他言語のクライアントや公開APIにはRESTやgRPCが向く。
- 中身はHTTPとJSON:queryはGET、mutationはPOSTで送られ、curlでも叩ける。複数の呼び出しは1リクエストにまとめられる。
- 本番で必ず設定するもの:
NODE_ENV=productionでもstackが消えるだけで、素のErrorのメッセージはそのまま返る。errorFormatterで隠す。 - 現行版:v11系の11.22.1。TypeScript 5.7.2以上が必要。OpenAPI生成の
@trpc/openapiは2026年3月から使えるが、まだalpha。
以下、仕組み→他方式との違い→実装→本番の注意→採用判断の順に説明します。
tRPCの仕組み:型の共有とHTTP上の実体
tRPCは「TypeScript Remote Procedure Call」の略です。型安全はビルド時の話で、通信そのものは普通のHTTPです。
import typeによる型共有と実行時通信の分離
サーバーでルーターを定義し、export type AppRouter = typeof appRouterで型だけを書き出します。クライアントはこれをimport typeで読み込みます。実行時にサーバーが型情報を送ることはありません。TypeScriptコンパイラがビルド時にサーバーとクライアントの型を突き合わせ、引数や戻り値の形がずれていればエラーにします。
HTTP通信の実体:queryはGET・mutationはPOST
通信はHTTPとJSONです。11.22.1のスタンドアロンサーバーを立ててcurlで叩くと、次のように返りました。procedureのパスがそのままURLになり、query(取得系)の入力はURLのinputパラメータにJSONで入ります。
$ curl "http://localhost:3000/user.byId?input=%7B%22id%22%3A%22u_1%22%7D"
{"result":{"data":{"id":"u_1","name":"Yamada","createdAt":"2026-10-31T00:00:00.000Z"}}}
$ curl "http://localhost:3000/user.byId,user.byId?batch=1&input=%7B%220%22%3A%7B%22id%22%3A%22a%22%7D%2C%221%22%3A%7B%22id%22%3A%22b%22%7D%7D"
[{"result":{"data":{"id":"a",...}}},{"result":{"data":{"id":"b",...}}}]
2つ目はhttpBatchLinkが作るバッチリクエストです。パスをカンマでつなぎ、batch=1を付けると、結果が配列で返ります。mutation(更新系)はPOSTで本文にJSONを入れて送ります。mutationをGETで呼ぶと405(METHOD_NOT_SUPPORTED)で拒否されます。
素のHTTPなのでcurlやPostmanでも呼べます。ただし、URLの形と{"result":{"data":...}}という包み方はtRPC独自です。他言語のクライアントから使う場合は、この形を手で合わせる必要があります。
tRPCとREST・GraphQL・gRPCの違い
4方式を、スキーマの有無・型の渡り方・通信形式で並べると次のとおりです。
| 観点 | REST | GraphQL | gRPC | tRPC |
|---|---|---|---|---|
| スキーマ | 任意(OpenAPI) | 必須(SDLかコード) | 通常は.proto | なし(TSの型) |
| クライアントの型 | 任意で生成 | 任意で生成 | 生成 | import type |
| 対応言語 | 問わない | 問わない | 多言語 | TypeScriptのみ |
| 通信 | HTTP・JSON | HTTP・JSON | HTTP/2・protobuf | HTTP・JSON |
| ブラウザから | そのまま | そのまま | gRPC-web等が必要 | そのまま |
| 向く用途 | 公開API | 多様な画面の取得 | サービス間通信 | TSのフルスタック |
違いの中心は「クライアントの型をどこから得るか」です。REST・GraphQLでは型の生成は任意で、gRPCは.protoからスタブを生成するのが標準です。tRPCには生成の手順がないので更新忘れによるずれは起きませんが、TypeScript以外の言語には型が渡りません。
RESTとの違い:URL設計の有無とキャッシュ
RESTはリソースをURLとHTTPメソッドで表し、型が欲しければOpenAPIを書いて生成します。tRPCはURL設計を開発者から隠し、関数呼び出しに置き換えます。社内のTypeScriptアプリなら、OpenAPIの管理と生成の工程がなくなるぶんtRPCが速く書けます。
一方、RESTは言語もクライアントも選びません。HTTPキャッシュはどちらも使えます。tRPCのqueryも普通のGETなので、公式のキャッシュの説明のとおりresponseMetaでCache-Controlを付けられます。ただし、tRPCは既定でバッチが有効なため、公開してよいデータと個人のデータが1つのレスポンスに混ざらないよう、splitLinkで分けるなどの注意が要ります。URLごとにキャッシュを設計するRESTの方が、CDNの設定は素直です。外部の開発者に提供するAPIは、今もRESTが第一候補です。設計の考え方はREST APIとGraphQLの違いと使い分けでも整理しています。
GraphQLとの違い:部分取得とスキーマの学習コスト
GraphQLは1つのエンドポイントに対し、クエリで欲しいフィールドだけを指定できます。画面ごとに必要なデータが違うアプリや、複数のクライアントが同じAPIを使う構成で強みが出ます。その代わり、スキーマとリゾルバの設計を覚える必要があり、リゾルバの書き方によってはN+1問題への対策(DataLoaderなど)も要ります。スキーマはSDLで書く方法と、コードから組み立てる方法があります。
tRPCにはクエリ言語がなく、戻り値はprocedureが返したオブジェクトそのものです。gRPCとGraphQLの比較はgRPCとGraphQLの違いは?役割分担・スキーマ運用・BFF併用で使い分けるにまとめています。
gRPCとの違い:対応言語とサービス間通信の契約
gRPCは、Googleが開発したRPCフレームワークです。Protocol Buffersの.protoファイルで契約を定義し、HTTP/2上でバイナリをやり取りします。Go・Java・Pythonなど言語の違うサービス同士をつなぐのに向き、双方向ストリーミングもあります。ブラウザから直接は呼べないため、gRPC-webやConnect(connectrpc)のような仲介が必要です。
言語をまたぐ契約が要るならgRPC、TypeScriptの中で完結するならtRPCです。gRPCの仕組みと実装はgRPCとは?UDPは使うのかをTCP・HTTP/3で実測で解説しています。
tRPC v11の使い方:サーバー定義からクライアント呼び出しまで
ここからのコードは、2026年10月31日に@trpc/server・@trpc/client 11.22.1、Zod 4.6.7、TypeScript 7.0.2で型チェックと実行を通したものです。インストールは次の1行です。
npm install @trpc/[email protected] @trpc/[email protected] [email protected]
v11はTypeScript 5.7.2以上を必要とします。公式の移行ガイドは、それより古い版ではインストール時にpeer dependencyのエラーが出るとしています。検証にはtsconfig.jsonで"strict": trueを有効にし、npx tsx server.tsで起動しました。
initTRPCによるルーターとprocedureの定義
initTRPCでインスタンスを作り、procedureを並べたルーターを書きます。認証が要るprocedureは、middlewareを挟んだprotectedProcedureから作ります。ルーターは副作用のないrouter.tsに置き、サーバーの起動は別ファイルに分けます。Next.jsやLambdaから読み込んだときに、余計なHTTPサーバーが立ち上がらないようにするためです。
// router.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
type Context = { userId: string | null };
const t = initTRPC.context<Context>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { userId: ctx.userId } });
});
const publicProcedure = t.procedure;
const protectedProcedure = t.procedure.use(isAuthed);
export const appRouter = t.router({
user: {
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => ({ id: input.id, name: 'Yamada', createdAt: new Date() })),
create: protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(({ input, ctx }) => ({ id: 'u_2', name: input.name, by: ctx.userId })),
},
});
export type AppRouter = typeof appRouter;
// server.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './router';
createHTTPServer({
router: appRouter,
maxBatchSize: 10,
createContext: ({ req }) => ({
// 動作確認用の固定トークン。本番ではトークンの署名と有効期限を検証する
userId: req.headers.authorization === 'Bearer demo' ? 'admin' : null,
}),
}).listen(3000);
user: { ... }のように、プレーンなオブジェクトを入れ子にするだけでサブルーターになります(v11の短縮記法)。createContextはリクエストごとに呼ばれ、ここで取り出した認証情報がmiddlewareとprocedureのctxに届きます。Bearer demoの判定は動作確認のための仮のもので、本番ではJWTの署名検証やセッションの照合に置き換えます(公式の認可の説明)。
入力検証は.input()に渡したスキーマが実行時に行います。公式のバリデータの説明にあるとおり、Zodのほか、Standard Schemaに準拠したValibotやArkTypeもそのまま渡せます。Zodの書き方はZodの使い方入門を参照してください。
クライアントからの呼び出し:createTRPCClientとhttpBatchLink
// client.ts
import { createTRPCClient, httpBatchLink, TRPCClientError } from '@trpc/client';
import type { AppRouter } from './router';
const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000', maxItems: 10 })],
});
const [a, b] = await Promise.all([
trpc.user.byId.query({ id: 'u_1' }),
trpc.user.byId.query({ id: 'u_9' }),
]);
try {
await trpc.user.create.mutate({ name: 'Sato' });
} catch (e) {
if (e instanceof TRPCClientError) {
console.log(e.message, e.data?.code, e.data?.httpStatus);
}
}
同じタイミングで出した2つのqueryは、httpBatchLinkが1つのHTTPリクエストにまとめます。認証ヘッダーなしのmutationはUNAUTHORIZED UNAUTHORIZED 401と表示されました。
v10のcreateTRPCProxyClientはcreateTRPCClientに改称されました。11.22.1でも旧名は別名として残っていますが、非推奨です。
バッチ上限の設定:maxBatchSizeとmaxItemsの整合
11.15.0(2026年3月)で、サーバー側にmaxBatchSizeが加わりました。既定は無制限で、1リクエストに何百ものprocedure呼び出しを詰められます。上限を3にしたサーバーへ4件のバッチを送ると、400でBatch call exceeds maximum sizeが返りました。
サーバーだけに上限を設けると、正規のクライアントのバッチまで弾かれます。型定義のコメントにも「クライアントのリンクにも同じかそれ以下の上限を設定すること」とあるので、httpBatchLinkのmaxItemsを同じ値以下にします。GETのバッチはURLが長くなるため、maxURLLengthで分割の目安も決めておくとプロキシの上限に当たりにくくなります。
型の不一致を止めるtscのエラー文の実例
サーバーの型とずれた呼び出しを書き、tscにかけた結果です。
trpc.user.byId.query({ id: 1 });
// error TS2322: Type 'number' is not assignable to type 'string'.
const u = await trpc.user.byId.query({ id: 'x' });
u.email;
// error TS2339: Property 'email' does not exist on type
// '{ id: string; name: string; createdAt: string; }'.
trpc.user.remove.mutate({ id: 'x' });
// error TS2339: Property 'remove' does not exist on type 'DecoratedProcedureRecord<...>'.
procedureを消したり改名したりすると、呼び出し側がすべてビルドエラーになります。補完が効かず型がanyになる場合、公式の移行ガイドは、エディタがプロジェクトに入っているTypeScriptを使っていない可能性を挙げています。
本番で詰まるtRPCのエラー処理と内部情報の漏れ
tRPCのエラーは、JSON-RPC 2.0に似た番号とHTTPステータスの両方で返ります。11.22.1で実際に返った組み合わせは次のとおりです。
| 状況 | code | HTTP | 番号 |
|---|---|---|---|
| 入力がスキーマ違反 | BAD_REQUEST | 400 | -32600 |
| middlewareで未認証 | UNAUTHORIZED | 401 | -32001 |
| 存在しないパス | NOT_FOUND | 404 | -32004 |
| mutationをGETで呼んだ | METHOD_NOT_SUPPORTED | 405 | -32005 |
| procedure内で例外 | INTERNAL_SERVER_ERROR | 500 | -32603 |
入力違反の場合は、Zodのエラー内容(Invalid input: expected string, received numberなど)がmessageに入ります。クライアントはこれを見てフォームのエラー表示に使えます。
NODE_ENV=productionでの例外メッセージの露出
開発時のレスポンスにはdata.stackにスタックトレースが入ります。tRPCはNODE_ENVがproductionかどうかで開発モードを決めるため、本番ではstackが消えます。
ただし消えるのはstackだけです。procedureの中でthrow new Error('db password=secret')とすると、NODE_ENV=productionでも次のように返りました。
{"error":{"message":"db password=secret","code":-32603,
"data":{"code":"INTERNAL_SERVER_ERROR","httpStatus":500,"path":"boom"}}}
DBドライバや外部APIの例外メッセージには、接続先のホスト名やSQLの一部が含まれることがあります。本番ではerrorFormatterでINTERNAL_SERVER_ERRORの文言を差し替え、元のメッセージはサーバーのログにだけ残します。errorFormatterは、ルーターを作っているinitTRPCのcreate()に追加します(別のinitTRPCを作っても効きません)。次のコードはその追加部分だけを抜き出したものです。
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
return { ...shape, message: 'Internal server error' };
}
return shape;
},
});
createHTTPServer({
router: appRouter,
onError({ error, path }) {
console.error('[trpc]', path, error.message);
},
});
この設定で、レスポンスは"message":"Internal server error"に変わり、ログには[trpc] boom db password=secretが出ました。TRPCErrorで意図して投げた400や401の文言はそのまま返るので、利用者向けのメッセージは失われません。
Date型が文字列になる問題とsuperjsonの設定
tRPCは既定でJSONを使います。サーバーでnew Date()を返すと、クライアントには"2026-10-31T00:00:00.000Z"という文字列で届きます。型も文字列として推論されるため、createdAt.getTime()と書くとProperty 'getTime' does not exist on type 'string'でビルドが止まります。型と実体はずれませんが、サーバー側の型(Date)とは別物になる点に注意してください。
DateやMap・Setをそのまま受け渡したいときは、transformerにsuperjsonを指定します。v11では、クライアント側の指定場所がリンクの中に移りました(v10ではクライアントの直下でした)。
// サーバー
import superjson from 'superjson';
const t = initTRPC.create({ transformer: superjson });
// クライアント(v11はリンク側に書く)
const client = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000', transformer: superjson })],
});
superjson 2.2.6で試すと、r.at instanceof Dateとr.tags instanceof Setがどちらもtrueになりました。通信上は{"json":{...},"meta":{"values":{"at":["Date"],"tags":["set"]}}}の形になるので、curlなどtRPCクライアント以外から読む場合はこの形を解釈する必要があります。
Next.js・React・AWS Lambdaへの組み込み
Next.js App RouterへのfetchRequestHandlerの配置
App RouterではRoute Handlerに公式が示すfetchアダプターを置きます。
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/router';
import { getUserIdFromRequest } from '@/server/auth';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: async () => ({ userId: await getUserIdFromRequest(req) }),
});
export { handler as GET, handler as POST };
getUserIdFromRequestは、トークンやセッションを検証して利用者IDを返し、検証に失敗したらnullを返す自前の関数です。ヘッダーがあるかどうかだけで判定すると、任意の文字列で認証を通過されます。queryはGET、mutationはPOSTで届くので、両方をエクスポートします。片方を忘れると、該当する種類のprocedureだけが405になります。
React統合:@trpc/tanstack-react-queryのqueryOptions
v11で加わった@trpc/tanstack-react-queryは、TanStack QueryのuseQueryにそのまま渡せるオプションを作る方式です。公式のセットアップ手順は、従来の@trpc/react-query(Classic)より新しい方式を推奨しています。
'use client';
import { useState } from 'react';
import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import type { AppRouter } from '@/server/router';
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({ links: [httpBatchLink({ url: '/api/trpc' })] }),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{children}
</TRPCProvider>
</QueryClientProvider>
);
}
export function UserName({ id }: { id: string }) {
const trpc = useTRPC();
const user = useQuery(trpc.user.byId.queryOptions({ id }));
const create = useMutation(trpc.user.create.mutationOptions());
return <button onClick={() => create.mutate({ name: 'Sato' })}>{user.data?.name}</button>;
}
Providersはクライアントコンポーネント('use client')なので、App Routerのlayout.tsxで子要素を包む形で使います。
Classicではtrpc.user.byId.useQuery()とtRPC独自のフックを呼んでいました。新方式はTanStack Queryの標準のフックにqueryOptions()を渡す形なので、TanStack Queryの資料がそのまま使えます。11.22.1の必要条件は@tanstack/react-query ^5.80.3とReact 18.2.0以上です。TanStack Query自体の既定値の注意点はTanStack Queryとは?React Queryとの違い・v5の使い方と既定値の落とし穴で扱っています。
AWS LambdaへのawsLambdaRequestHandlerの組み込み
Lambdaには@trpc/server/adapters/aws-lambdaのawsLambdaRequestHandlerを使います。API GatewayのREST API(v1)とHTTP API(v2)、Lambda Function URLに対応します。
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
import { appRouter } from './router';
export const handler = awsLambdaRequestHandler({
router: appRouter,
createContext: () => ({ userId: null }),
});
バッチで複数のprocedureが1回の起動にまとまるため、1件の重い処理がほかの呼び出しの応答も遅らせます。Lambdaのタイムアウトは、バッチ全体で一番遅い処理を基準に決めます。
このハンドラはレスポンス全体ができてから返す通常版で、SSEやhttpBatchStreamLinkの逐次配信にはなりません。Lambdaで逐次配信したい場合は、公式のLambdaアダプターの説明にあるawsLambdaStreamingRequestHandlerをawslambda.streamifyResponse()で包みます。配信経路はLambda Function URLか、responseTransferMode: STREAMを設定したAPI GatewayのREST APIに限られます。常時接続が前提のWebSocketは、Lambdaではなくコンテナで常駐させる方が扱いやすくなります。
/// <reference types="aws-lambda" />
import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda';
import { appRouter } from './router';
export const handler = awslambda.streamifyResponse(
awsLambdaStreamingRequestHandler({ router: appRouter, createContext: () => ({ userId: null }) }),
);
v10からv11で変わった点と11.22までの追加機能
v11.0.0は2025年3月21日に公開されました。v10の記事やサンプルとの食い違いの多くは、次の変更が原因です。
- transformerの指定場所:クライアント直下からリンクの中へ。公式の移行ガイドで「breaking」とされた2件のうちの1つです。
- React Queryの必要版:
@trpc/react-queryを使う場合は@tanstack/react-queryv5が必須になりました。もう1件の「breaking」です。 - クライアント関数名:
createTRPCProxyClientはcreateTRPCClientへ改称されました。旧名は非推奨の別名として残っています。 - TypeScriptの最低版:5.7.2以上。
- サブスクリプション:WebSocketに加え、SSEに対応しました。ハンドラは
Observableではなくasync generatorで書くのが標準になりました。 - ストリーミングとバイナリ:
httpBatchStreamLinkで結果を届いた順に返せます。FormDataやFile、octetInputParserでのバイナリ入力も扱えます。 - interopモードの削除:v9互換の
.interop()はなくなりました。
v11に入ってからもマイナー版で機能が増えています。2026年に入ってからでは、11.13.2でhttpBatchStreamLinkのstreamHeaderオプション、11.15.0でmaxBatchSize、11.17.0でサブスクリプション型の推論ヘルパーが加わりました。11.19.0(2026年9月16日)の次の安定版は11.22.1で、11.20と11.21は安定版として公開されていません。
OpenAPIを生成してtRPCを外部に開く:@trpc/openapi(alpha)
「tRPCはOpenAPIを出せないので公開APIには向かない」という評価は、2026年3月に公開された@trpc/openapiで前提が変わりつつあります。ルーターの型を静的に解析し、OpenAPI 3.1のドキュメントを生成するパッケージです。コードは実行しません。公式ドキュメントには「This package is in alpha. APIs may change without notice.」と明記されています。
import { generateOpenAPIDocument } from '@trpc/openapi';
import { writeFileSync } from 'node:fs';
const doc = await generateOpenAPIDocument('./router.ts', {
exportName: 'AppRouter',
title: 'Demo API',
version: '1.0.0',
});
writeFileSync('openapi.json', JSON.stringify(doc, null, 2));
上のルーターから生成すると、openapi: 3.1.1で/user.byIdはGET、/user.createはPOSTになりました。GETの入力はinputというクエリパラメータにJSONで入る形です。.output()を書いていなくても、戻り値の型からcreatedAtがformat: date-timeの文字列として出力されました。
2026年10月31日時点の11.22.1-alphaで試すと、実務で引っかかる点が3つありました。
- TypeScript 7では入らない:peer dependencyが
typescript >=5.7.2 <7です。npmのlatestは7.0.2なので、そのままnpm installすると依存解決エラー(ERESOLVE)になります。TypeScript 6.0.3では入りました。@trpc/server本体は7.0.2で型チェックが通ります。 - CLIのコマンドが起動しない:package.jsonのbinは
dist/cli.jsを指していますが、配布物にあるのはdist/cli.mjsです。npx trpc-openapiは「could not determine executable to run」で失敗しました。node node_modules/@trpc/openapi/dist/cli.mjs ./router.tsで直接呼ぶか、上のプログラムAPIを使います。 - サブスクリプションは出力されない:公式も「currently excluded」としています。
レスポンスは{"result":{"data":...}}で包まれたままで、普通のREST APIの見た目にはなりません。社内の他言語クライアントやAIエージェントから呼ぶ用途なら試す価値があります。一方、外部に公開して長く互換性を守る契約としては、alphaのうちは採用しません。そうした用途では、OpenAPIを先に書くスキーマファーストの進め方が確実です。
tRPCを採用すべきでない場面
次のどれかに当てはまるなら、tRPCは選びません。型の共有という利点が得られないまま、tRPC独自の通信形式という制約だけが残るからです。
- TypeScript以外のクライアントが主役:iOS(Swift)やAndroid(Kotlin)のネイティブアプリには型が渡りません。OpenAPI経由で型を作るなら、最初からRESTで作る方が手戻りがありません。
- 第三者に公開するAPI:版の管理、HTTPキャッシュ、言語を問わない利用が求められます。RESTとOpenAPI、またはGraphQLを使います。
- 言語の違うマイクロサービス間:契約を.protoで固定できるgRPCの方が向きます。
- 型の配布と更新を管理できない:別リポジトリでも、公式FAQのとおり型を非公開npmパッケージで配れば使えます。ただし、サーバーとクライアントの版を合わせて更新する運用が回らないなら、ずれを検出できるというtRPCの保証が失われます。
逆に、Next.jsなどで1つのチームがフロントとバックエンドを同じリポジトリで開発するなら、tRPCの効果は大きくなります。その構成を最初からそろえた雛形がT3 StackやBetter-T-Stackです。既存のREST APIがある場合は、全部を移す必要はなく、新しいエンドポイントからtRPCにする段階的な移行も公式が案内しています。
既存システムとの連携を含めたAPIの方式選定や実装は、API開発・システム連携でご相談いただけます。
よくある質問
tRPCとRESTはどちらを使うべきですか?
クライアントもサーバーもTypeScriptで、サーバーの型をクライアントが参照できる(モノレポ、または型をnpmパッケージで配る)ならtRPC、外部に公開するAPIやTypeScript以外のクライアントがあるならRESTです。社内のWeb画面向けはtRPC、外部向けはRESTと、同じバックエンドで両方を持つ構成もあります。
tRPCとGraphQLの違いは何ですか?
GraphQLはSDLでスキーマを定義し、クライアントがクエリで欲しいフィールドを選びます。tRPCにはスキーマ言語もクエリ言語もなく、procedureの戻り値をそのまま受け取ります。複数の種類のクライアントが同じAPIを使うならGraphQL、自社のTypeScriptフロント専用ならtRPCです。
tRPCとgRPCは何が違いますか?
gRPCは.protoで契約を定義し、HTTP/2上でバイナリを送る多言語向けの方式で、ブラウザからは仲介が必要です。tRPCはTypeScript専用で、HTTPとJSONでブラウザから直接呼べます。多言語のバックエンド間ならgRPC、TypeScriptのフロントとバックエンドの間ならtRPCです。
tRPCのサブスクリプションはSSEとWebSocketのどちらを使いますか?
v11はどちらにも対応しています。SSEはhttpSubscriptionLinkを使い、通常のHTTPサーバーのままで動きます。WebSocketはwsLinkと専用のWebSocketサーバーが必要です。公式は、迷ったらSSEを勧めています。クライアントからサーバーへ頻繁に送る双方向の通信が要る場合はWebSocketを選びます。WebSocket自体の仕組みはWebSocketとは?で解説しています。
tRPCの最新バージョンはいくつですか?
2026年10月31日時点のnpmの最新版は11.22.1(2026年10月28日公開)です。TypeScript 5.7.2以上が必要です。npmのv10タグは2026年10月31日時点で10.45.4を指しており、新規に始めるならv11を使います。v10から移る場合は、transformerの指定場所の変更とcreateTRPCClientへの改称を公式の移行ガイドで確認してください。