【T3 Stack入門】tRPCの基礎 vol.2 (create-t3-appコード解説編)
はじめに
tRPCは、TypeScriptを使用したフルスタック開発において、クライアントとサーバー間の型安全なAPI通信を実現するライブラリです。
この記事では、T3 Stackのプロジェクト雛形であるcreate-t3-appで作成されたリポジトリのコードを読み解きながら、tRPCの基本概念について解説します。
tRPCのメリットについては他のAPI構築手法と比較した記事をvol.1で書きましたので、ご興味のある方はご覧ください。
なお、本記事のコード解説ではNext.jsのApp Routerを使用しますが、App Routerの基本的な機能の解説ついては省略させていただきます。App Routerの基本について知りたい方は以下の記事たちをお読みいただくと、本記事への理解が深まるかと思います。
create-t3-appについて
create-t3-appは、Next.js、TypeScript、tRPCなどのT3 Stackの技術スタックを組み合わせたプロジェクト雛形です。インストール時に使いたいライブラリを選ぶだけで、T3 Stackを用いたアプリ開発ができる環境を提供してくれます。
create-t3-appをインストールすると、すでにtRPCがNext.jsに組み込まれており、サンプルProcedure(=APIのエンドポイント)を実行できる状態まで実装されています。サンプルProcedureに関連するコードを読み解くことでtRPCの基本構造を理解していきましょう。
create-t3-appコマンドで雛形をインストールする手順
コマンド実行
下記コマンドをターミナルで実行してパッケージをインストールしてください。最新版(latest)のT3 Stackのプロジェクト雛形を使うことができます。
$ npm create t3-app@latest
ライブラリの選択
コマンドを実行するとプロジェクト名や使うライブラリの選択についての質問が表示されます。
本記事では、以下を選択したリポジトリを作成しました。以降のコード例ではこちらのリポジトリのコードを参照しています。
質問 | 選択した回答 |
---|---|
What will your project be called? | 任意のプロジェクト名 |
Will you be using Typescript or JavaScript? | TypeScript |
Will you be using Tailwind CSS for styling? | Yes |
Would you like to use tRPC? | Yes |
What authentication provider would you like to use? | NextAuth.js |
What database ORM would you like to use? | Prisma |
Would you like to use Next.js App Router? | Yes |
What database provider would you like to use? | SQLite(LibSQL) |
Should we initialize a Git repository and stage the changes? | Yes |
Should we run ‘npm install’ for you? | Yes |
What import alias would you like to use? | ~/ |
ローカルサーバーの起動
インストールが完了したら、以下を入力して作成したプロジェクトのディレクトリに入ります。
$ cd *作成したプロジェクト名*
プロジェクトのルートディレクトリに入ったら、以下を入力します。このコマンドを実行すると、Prismaスキーマがデータベースと同期され、スキーマに基づいてPrisma Client用のTypeScript型が生成されます。
$ npm run db:push
認証プロバイダーの設定が完了したら、以下を入力してローカルサーバーを起動します。
$ npm run dev
http://localhost:3000/ にブラウザからアクセスし、「Create T3 App」と書かれた画面(Next.jsのルートページであるsrc/app/page.tsx
)が表示されれば環境構築は完了です。
サンプルProcedureの確認
この雛形にはあらかじめサンプルProcedure(=APIのエンドポイント)を実行できる状態まで実装されています。どのようなProcedureが使われているかルートページ内を確認してみましょう。
まず、「Sign in」ボタンを押してDiscordのサインインを行います。
サインインが完了しルートページにリダイレクトされると、「Logged in as ユーザー名」と入力フォームが表示されます。
以下がサンプルProcedureの内容一覧と、画面内での実装箇所です。実際にフォームに入力してsubmitボタンを押すと、create
とgetLatest
の動きがわかるかと思います。
今回はこれらのサンプルProcedureを呼び出すために、雛形でどのように設定されているか関連ファイルのコードを解説していきます。
画面に実装されているサンプルProcedure一覧
Procedure名 | 機能 | 画面上の動作 |
---|---|---|
hello |
簡単な挨拶(Hello from 設定した文字列)を返します。 | ページにアクセスしたときに表示する |
create |
新しい投稿を作成します。 | フォーム入力後、submitボタン押下時に投稿が実行される |
getLatest |
最新の投稿を取得します。 | 最新の投稿内容をフォーム上に表示する。まだ投稿が存在しない場合は「You have no posts yet.」を表示する |
tRPCの基本用語と全体のフロー
具体的な実装を見ていく前に、まずtRPCの基本用語と全体の流れを把握しておきましょう。
tRPCの基本用語解説
Procedure(プロシージャ)
tRPCのAPIエンドポイントに対応する関数で、クライアントからのリクエストに対して、必要な処理(例:データベースからのデータ取得やデータ更新)を実行し、結果を返します。
Middleware(ミドルウェア)
Procedureの実行前後に挿入される処理です。リクエストやレスポンスの加工や認証、エラーハンドリングなどに使用されます。
Router(ルーター)
複数のProcedureをグループ化し、構造化するためのオブジェクトです。Routerは他のSub Routerをネストすることができます。これにより、複数のRouterを組み合わせて、大規模なAPIを整理して構築することが容易になります。
Adapter(アダプター)
tRPCサーバーとクライアント間の通信を橋渡しする役割を持ちます。クライアントからのリクエストを受け取ってProcedureに転送し、そのレスポンスをクライアントに返します。
Context(コンテキスト)
リクエストごとに生成される、Procedure内で利用可能なデータや機能を含むオブジェクトです。Procedure内で使用するデータやサービス(例: ユーザーのセッション情報、データベース接続)を提供します。
tPRCのフロー図
上記の構成要素が実行されるフローを図にしました。
このフローを念頭に置いた上で、create-t3-appで作られたリポジトリで実際のコードを確認していきましょう。
ディレクトリ構成
create-t3-appで生成されたプロジェクトの主要なディレクトリ構造は以下となります。
# tRPCに関連する部分を中心に抜粋
src
├── app # Next.js App Routerを使用したフロントエンド部分
│ ├── _components
│ │ └── post.tsx # 投稿関連のコンポーネント(ClientComponents)
│ ├── api
│ │ └── trpc
│ │ └── [trpc]
│ │ └── route.ts # tRPCのHTTPハンドラー(Next.jsのRoute Handlers)
│ ├── layout.tsx # アプリケーション全体のレイアウトコンポーネント
│ └── page.tsx # アプリケーションのルートページコンポーネント(ServerComponents)
├── server # バックエンド部分(tRPCサーバーサイドロジック)
│ └── api
│ ├── root.ts # 全tRPCRouterを統合するRoot Router
│ ├── routers
│ │ └── post.ts # 投稿関連のtRPCProcedureを定義するSub Router
│ └── trpc.ts # tRPCの初期化と共通設定
└── trpc # tRPCのクライアント・サーバー共通設定とユーティリティ
├── query-client.ts # TanStack Queryのクライアント設定
├── react.tsx # クライアントサイドtRPC用Reactフックとプロバイダー
└── server.ts # サーバーサイドtRPCクライアント初期化
Next.jsはサーバーサイドの処理も可能なフレームワークなので、バックエンドとフロントエンドのコードが同一のリポジトリ内に共存しています。src
直下の3つのディレクトリの役割を大まかに分けると以下になります。
主要ディレクトリの役割
src/app
: Next.js App Routerを使用したフロントエンド部分を担うディレクトリです。コンポーネントやページが配置されています。
src/server
: tRPCのサーバーサイドのロジック(API構築やDB接続)が入ったバックエンド部分を担うディレクトリです。
src/trpc
: フロントエンドとバックエンドの橋渡しとなる役割を担うディレクトリです。tRPCをフロントエンドのServer Components / Client Componentsで呼ぶための共通設定が行われています。
では、主要ディレクトリ内の各ファイルでどのような処理を行なっているか追っていきます。
src/server
:tRPCの初期化、APIの作成
このsrc/server
ディレクトリには以下3つのファイルが収められており、tRPCの初期化、APIの作成、ルーティング設定が行われています。
├── server # バックエンド部分(tRPCサーバーサイドロジック)
│ └── api
│ ├── root.ts # 全tRPCRouterを統合するRoot Router
│ ├── routers
│ │ └── post.ts # 投稿関連のtRPCProcedureを定義するSub Router
│ └── trpc.ts # tRPCの初期化と共通設定
src/server/api/trpc.ts
でのtRPC初期化
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { db } from "~/server/db";
// コンテキストの作成関数
export const createTRPCContext = async (opts: { headers: Headers }) => {
return {
db,
...opts,
};
};
// tRPCを初期化し、tオブジェクトを作成
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// サーバーサイドの呼び出し元を作成する関数
export const createCallerFactory = t.createCallerFactory;
// tRPC APIで新しいルーターとサブルーターを作成する関数
export const createTRPCRouter = t.router;
// Procedureの実行時間を計測し、開発環境で人工的な遅延を追加するMiddleware
const timingMiddleware = t.middleware(async ({ next, path }) => {
const start = Date.now();
if (t._config.isDev) {
// artificial delay in dev
const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
const result = await next();
const end = Date.now();
console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
return result;
});
// 認証が不要なProcedure
export const publicProcedure = t.procedure.use(timingMiddleware);
// 認証が必要なProcedure
export const protectedProcedure = t.procedure
.use(timingMiddleware)
.use(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});
ここではtRPCのContextの作成、tRPCの初期化、RouterやProcedureの定義が行われます。
createTRPCContext
関数:
リクエスト処理時に利用可能な情報(セッション、データベース接続、ヘッダーなど)を提供します。後述のt
オブジェクトを利用して、 protectedProcedure
内でctx
として参照されています。
t
オブジェクトの作成:
tRPCの中核となるオブジェクトで、Router、Procedure、Middlewareの作成に使用されます。createTRPCContext
の型を持っているため、tオブジェクトを通じて作成されるProcedureやミドルウェアは、このコンテキストにアクセスできます。
例えば、protectedProcedure
では ctx.session
を参照してユーザーの認証状態をチェックしています。
また、transfomer
に設定されているsuperjson
は、JavaScriptの複雑なデータ構造をJSON形式でシリアライズ/デシリアライズするためのライブラリです。標準のJSONでは扱えない型(Date, Map, Set, BigInt など)をサポートし、クライアントとサーバー間でこれらの複雑な型を安全にやり取りできます。
errorFormatter
に設定されているZodError
は、Zodライブラリによるバリデーションが失敗した際に発生するエラーの型です。ZodErrorには、どのフィールドでどのようなバリデーションエラーが発生したかの詳細情報が含まれており、クライアント側に具体的なエラー内容を伝えることができます。
ProcedureオブジェクトとMiddlewareの利用:
publicProcedure
と protectedProcedure
の2種類が定義されています。publicProcedure
は認証不要、protectedProcedure
は認証済みユーザー用です。
t.procedure.use(*ミドルウェアの処理*)
でMiddlewareを入れることができます。このコードでは実行時間の計測と開発環境での人工的な遅延を追加するtimingMiddleware
と、protectedProcedure
では認証チェックもMiddlewareとして実装されています。
これらのProcedureオブジェクトは、src/server/api/routers/post.ts
のRouter定義時に使用されています。
src/server/api/routers/post.ts
でのProcedure定義
import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "~/server/api/trpc";
// postRouterの定義:投稿に関連する操作をまとめたSub Router
export const postRouter = createTRPCRouter({
// 認証が不要なProcedure:誰でもアクセス可能
hello: publicProcedure
// 入力スキーマの定義:textという文字列を受け取る
.input(z.object({ text: z.string() }))
// クエリの実装:入力されたtextを使って挨拶メッセージを返す
.query(({ input }) => {
return {
greeting: `Hello ${input.text}`,
};
}),
// 認証が必要なProcedure:認証済みユーザーのみアクセス可能
create: protectedProcedure
// 入力スキーマの定義:nameという1文字以上の文字列を受け取る
.input(z.object({ name: z.string().min(1) }))
// ミューテーション(データ変更操作)の実装
.mutation(async ({ ctx, input }) => {
// データベースに新しい投稿を作成
return ctx.db.post.create({
data: {
name: input.name,
// 現在のユーザーを投稿の作成者として関連付け
createdBy: { connect: { id: ctx.session.user.id } },
},
});
}),
// 認証が必要なProcedure:最新の投稿を取得
getLatest: protectedProcedure.query(async ({ ctx }) => {
// 現在のユーザーが作成した投稿の中で最新のものを取得
const post = await ctx.db.post.findFirst({
orderBy: { createdAt: "desc" },
where: { createdBy: { id: ctx.session.user.id } },
});
// 投稿が見つかればその投稿を、見つからなければnullを返す
return post ?? null;
}),
// 認証が必要なProcedure:秘密のメッセージを取得(画面には未実装)
getSecretMessage: protectedProcedure.query(() => {
// 認証済みユーザーのみがアクセスできる秘密のメッセージを返す
return "you can now see this secret message!";
}),
});
このファイルでは、postRouter
という名前のRouterを作成し、その中に4つのProcedureを定義しています。
Procedure名 | 機能 | 認証不要or 認証必要 |
---|---|---|
hello |
簡単な挨拶(Hello from 設定した文字列)を返します。 |
publicProcedure (認証不要) |
create |
新しい投稿を作成します。 |
protectedProcedure (認証必要) |
getLatest |
最新の投稿を取得します。 |
protectedProcedure (認証必要) |
getSecretMessage |
認証済みユーザーのみがアクセスできる秘密のメッセージを返します。 |
protectedProcedure (認証必要) |
input
内の処理:
input
メソッドは、Zodを使用してProcedureの入力データの構造と型を定義します。例えばhello
では、.input(z.object({ text: z.string() }))
で、text
フィールドにstring
型の入力を指定しています。これにより、実行時の入力データの検証と、開発時の型安全性が確保されます。
query
とは:
データの読み取り操作を定義します。サーバーからデータを取得するために使用されています。
mutation
とは:
データの書き込みや更新操作を定義します。サーバー側でデータを変更する際に使用され、例えば新しい投稿の作成やユーザー情報の更新などに適しています。
getLatest
で使われているctx
とは:
ctx
は、現在のユーザーセッション情報やデータベース接続など、Procedure内で利用可能なContextのオブジェクトです。例えば、ctx.session.user.id
で現在のユーザーIDにアクセスしたり、ctx.db
でデータベース操作を行っています。
続いて、このファイルで作られたSub RouterであるPostRouter
をsrc/server/api/root.ts
で一つのRouterに集約します。
src/server/api/root.ts
でSub Routerをまとめる
import { postRouter } from "~/server/api/routers/post";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
// /api/routersディレクトリに定義された個別のルーター(この場合はpostRouter)をまとめる
export const appRouter = createTRPCRouter({
post: postRouter,
// 将来的に新しいSub Routerを追加する場合、ここに追加します
});
// APIの型定義をエクスポート:クライアント側での型安全性を確保
export type AppRouter = typeof appRouter;
// サーバーサイドでAPIを直接呼び出すため関数
export const createCaller = createCallerFactory(appRouter);
このファイルでは、各Sub Routerを1つのRouterにまとめます。
appRouter
でSub Routerを集約:
/api/routers
ディレクトリに定義された個別のルーター(この場合はpostRouter
)をまとめる役割を果たします。将来的に新しいルーターを追加する際は、ここに手動で追加する必要があります。
AppRouter
型のエクスポート:
appRouter
の型定義をエクスポートしています。これにより、クライアント側でAPIの型情報を利用できるようになり、型安全性が向上します。
GraphQLやOpenAPIでは別途スキーマファイルを作成する必要がありますが、このAppRouter
型のインポートだけで型情報をクライアントと共有できるシンプルさが、tRPCの魅力のひとつです。
createCaller
関数:
これを使用することで、サーバーサイドでAPIを直接呼び出すことができます。最終的にはServerComponentsでtrpc.post.all()
のように、定義されたProcedureを呼び出すことが可能になります。このサーバーサイドで直接呼び出すための実装はsrc/trpc/server.ts
にて行われています。
src/trpc
: tRPCをフロントエンドで呼び出すための設定
ここまで、APIの実装とルーティングの流れを確認しました。次に、フロントエンドでtRPCのAPIを呼び出すための設定について、src/trpc
ディレクトリの各ファイルを見ていきます。
└── trpc # tRPCとTanstack Queryのクライアント・サーバー共通設定とユーティリティ
├── query-client.ts # TanStack Queryのクライアント設定
├── react.tsx # クライアントサイドtRPC用Reactフックとプロバイダー
└── server.ts # サーバーサイドtRPCクライアント初期化
このsrc/trpc
ディレクトリではサーバーサイド(ServerComponents)とクライアントサイド(ClientComponents)でtRPCのAPIを呼び出すための設定ファイルが入っています。
tRPCで実行されたAPIのキャッシュやデータ管理を、Tanstack Query(データフェッチング、キャッシングを効率的に管理するためのライブラリ)が担っています。
具体的なコードを見ていく前に、ServerComponentsとClientsComponentsのtRPCのAPI呼び出し方法の違いについて説明します。
ServerComponentsとClientsComponentsのAPI実行方法の違い
ServerComponents:
サーバー上で実行されるため、tRPC関数を直接呼び出すことができます。データのfetchやprefetchを行うときは、Tanstack Queryを利用してキャッシュしたデータをClientComponentsに受け渡します。
ClientComponents:
ブラウザで実行されるため、HTTPリクエストを介してAPIエンドポイントを呼び出します。これらのリクエストは、Next.jsのAPIルートを通じて処理されます。Tanstack Queryを使用してデータのフェッチング、キャッシング、および状態管理を行っています。
この違いを頭の片隅に置いた上で、各ファイルの中身を読み進めていきましょう。
src/trpc/query-client.ts
でTanStack QueryのQueryClientを設定
import {
defaultShouldDehydrateQuery,
QueryClient,
} from "@tanstack/react-query";
import SuperJSON from "superjson";
// QueryClientを作成する関数
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// データを再フェッチするまでの時間を設定
staleTime: 30 * 1000
},
// サーバーからクライアントへのデータ転送(デハイドレーション)の設定
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === "pending",
},
// クライアントでのデータ復元(ハイドレーション)の設定
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
});
このファイルでは、TanStack Query(旧React Query)のクライアント設定を行います。
このファイルでは、QueryClient
のインスタンス生成を通じて、ServerComponentsでフェッチしたデータをClientComponentsへ渡すためのcreateQueryClient
関数の定義を行っています。
staleTime
の設定:
データを再フェッチするまでの時間を設定しています。同じデータに対する新たなクエリは、キャッシュされたデータを使用し、再フェッチは行いません。
dehydrate
の設定:
ServerComponentsで取得したデータをシリアライズし、ClientComponentsに転送できる形式に変換しています。
shouldDehydrateQuery
の中にpending
状態のクエリも含めることで、ServerComponentsで開始されたが完了していないクエリデータも転送可能になります。これにより、ClientComponents側でクエリが進行中であることを示すローディング状態や部分的に取得したデータ表示が可能になります。
hydrate
の設定:
ServerComponentsで取得したデータをClientComponentsで再構築し、使用可能な状態にする際の設定です。SuperJSONのdeserializeメソッドを使用して、シリアライズされたデータを元の複雑なデータ構造(Date、Map、Setなど)に復元します。これにより、サーバーサイドのデータをクライアントサイドで正確に再現できます。
では、このcreateQueryClient
関数を使って、ServerComponentsでtRPCを呼ぶための設定がどのように実装されているのかsrc/trpc/server.ts
を見ていきましょう。
src/trpc/server.ts
でサーバーコンポーネントから呼ぶためのエントリーポイントを定義
import "server-only";
import { createHydrationHelpers } from "@trpc/react-query/rsc";
import { headers } from "next/headers";
import { cache } from "react";
import { createCaller, type AppRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
import { createQueryClient } from "./query-client";
// サーバーサイドのtRPCコンテキストを作成し、キャッシュする
const createContext = cache(() => {
const heads = new Headers(headers());
// `x-trpc-source`ヘッダーを設定して、リクエストがRSCから来ていることを明示
heads.set("x-trpc-source", "rsc");
return createTRPCContext({
headers: heads,
});
});
// Tanstack Queryのクエリクライアントを生成し、キャッシュする
const getQueryClient = cache(createQueryClient);
// tRPCの呼び出し関数を作成
const caller = createCaller(createContext);
// tRPCとTanstack Queryを統合するヘルパー関数
export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient
);
このファイルでは、tRPCとTanstack Queryを統合して、サーバーサイドでのデータフェッチとクライアントサイドでのハイドレーションを行うための設定を行なっています。
ServerComponentsでは、このファイルで作られたapi
オブジェクトとHydrate
コンポーネントを利用してtRPCを実行します。
createContext
の作成:
tRPCのコンテキストにx-trpc-source
ヘッダーを "rsc" (React Server Component) に設定します。これにより、tRPC がリクエストの出所を識別できます。
getQueryClient
でクエリクライアント作成:
Tanstack Queryのクエリクライアントを生成する関数です。このクエリクライアントは、サーバーサイドでフェッチしたデータをクライアントサイドのキャッシュに効率的に保存し、管理するために使用されます。
cache
関数でラップされているため、同一リクエスト内での再計算を防ぎ、効率的なクライアント生成を実現します。
ヘルパー関数createHydrationHelpers<AppRouter>
:
RSCでtRPCを使用するためのヘルパー関数createHydrationHelpers
が、api
オブジェクトとHydrate
コンポーネントを作成します。
ServerComponentsはこのapi
オブジェクトを通じてtRPCクエリを実行できます。プリフェッチされたデータをTanstack QueryのQueryClientのキャッシュに格納してくれる仕組みになっています。
HydrateClient
コンポーネントは、サーバーサイドで取得したデータをクライアントサイドにハイドレートするために使用されます。
ここで型指定されているAppRouter
型は、src/server/api/root.ts
で定義されているAPIの型です。このAppRouter
型を指定することで、Server ComponentでtRPCプロシージャの入力と出力の型が正確に推論されます。
src/trpc/react.tsx
でtRPCとTanStack Queryを統合したプロバイダーを作成
"use client";
import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import { useState } from "react";
import SuperJSON from "superjson";
import { type AppRouter } from "~/server/api/root";
import { createQueryClient } from "./query-client";
let clientQueryClientSingleton: QueryClient | undefined = undefined;
// クエリクライアントを取得または作成する関数
const getQueryClient = () => {
if (typeof window === "undefined") {
// サーバーサイド: 常に新しいクエリクライアントを作成
return createQueryClient();
}
// ブラウザ: シングルトンパターンを使用して同じクエリクライアントを保持
return (clientQueryClientSingleton ??= createQueryClient());
};
// tRPCクライアントの作成(AppRouter型を使用して型安全性を確保)
export const api = createTRPCReact<AppRouter>();
// ルーター入力の型推論ヘルパー
export type RouterInputs = inferRouterInputs<AppRouter>;
// ルーター出力の型推論ヘルパー
export type RouterOutputs = inferRouterOutputs<AppRouter>;
// tRPCとTanstack Queryを統合するプロバイダーコンポーネント
export function TRPCReactProvider(props: { children: React.ReactNode }) {
// クエリクライアントの取得
const queryClient = getQueryClient();
// tRPCクライアントの作成
const [trpcClient] = useState(() =>
api.createClient({
links: [
// ログの設定(コンソールに操作ログを表示させるデバッグ機能)
loggerLink({
enabled: (op) =>
process.env.NODE_ENV === "development" ||
(op.direction === "down" && op.result instanceof Error),
}),
// HTTP経由でtRPCリクエストをバッチ処理し、ストリーミングするための設定
unstable_httpBatchStreamLink({
transformer: SuperJSON,
url: getBaseUrl() + "/api/trpc",
headers: () => {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
}),
],
})
);
// QueryClientProviderとapi.Providerでラップしたコンポーネントを返す
return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{props.children}
</api.Provider>
</QueryClientProvider>
);
}
// ベースURLを取得する関数(環境に応じて適切なURLを返す)
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
このファイルでは、tRPCとTanStack Queryを統合し、Next.jsアプリケーション全体でのデータフェッチングと状態管理のための基盤を作っています。
このファイルは主にClient Componentsのための設定を行っています。具体的には、tRPCクライアントの初期化、Tanstack Queryの設定、そしてこれらを統合するTRPCReactProviderの提供など、クライアントサイドでのデータフェッチングと状態管理のための基盤を整えています。
一方、Server Componentsとの連携は、getQueryClient
関数内でサーバーサイドの判定を行い、SSRやRSC(React Server Components)でのデータフェッチに対応するQueryClientを生成することで実現しています。
getQueryClient
関数:
サーバーサイド(typeof window === "undefined"
)では、各リクエストに対して新しい QueryClient
インスタンスを作成します。これにより、異なるユーザーやリクエスト間でのデータの混在を防ぎます。
クライアントサイドでは、シングルトンパターンを使用して単一の QueryClient
インスタンスを維持します。これにより、アプリケーション全体で一貫したキャッシュ管理が可能になります。
TRPCReactProvider
コンポーネント:
このコンポーネントは、tRPCとTanstack Queryの機能をアプリケーション全体で利用可能にします。
tRPCのプロバイダーapi.Provider
をTanstack QueryのプロバイダーQueryClientProvider
でラップすることで、クライアントサイドでのtRPCの呼び出し時にTanstack Queryのキャッシュを活用することができます。
src/app
: Sever Components / Client ComponentsでAPIを呼ぶ
このapp
ディレクトリで行っているのは主に以下2点です。
- Client ComponentsでtRPCを実行するための設定(Route Handlersの作成、
TRPCReactProvider
でアプリケーション全体をラップ) - Sever Components / Client ComponentsでサンプルProcedureの呼び出し
├── app # Next.js App Routerを使用したフロントエンド部分
│ ├── _components
│ │ └── post.tsx # 投稿関連のコンポーネント(ClientComponents)
│ ├── api
│ │ └── trpc
│ │ └── [trpc]
│ │ └── route.ts # tRPCのHTTPハンドラー(Next.jsのRoute Handlers)
│ ├── layout.tsx # アプリケーション全体のレイアウトコンポーネント
│ └── page.tsx # アプリケーションのルートページコンポーネント(ServerComponents)
まずは、Client ComponentsでtRPCを実行するための設定ファイルから確認していきます。
src/app/layout.tsx
でTRPCReactProvider
でアプリケーション全体をラップする
import "~/styles/globals.css";
import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";
import { TRPCReactProvider } from "~/trpc/react";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={`${GeistSans.variable}`}>
<body>
<TRPCReactProvider>{children}</TRPCReactProvider>
</body>
</html>
);
}
src/trpc/react.tsx
で作成した<TRPCReactProvider>
でアプリケーション全体をラップすることにより、tRPCクライアントが利用可能になります。これにより、すべてのコンポーネントでtRPCのクエリやミューテーションを使用できるようになります。
src/app/api/trpc/[trpc]/route.ts
でAPIルートを定義
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { type NextRequest } from "next/server";
import { env } from "~/env";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
// リクエストごとのtRPCコンテキストを作成する関数
const createContext = async (req: NextRequest) => {
return createTRPCContext({
headers: req.headers,
});
};
// tRPCリクエストを処理するハンドラー関数
const handler = (req: NextRequest) =>
fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: () => createContext(req),
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
);
}
: undefined,
});
// GETとPOSTリクエストの両方に対してhandlerをエクスポート
// ClientComponentsからのtRPCリクエスト(GETまたはPOST)が
// このhandlerによって処理される
export { handler as GET, handler as POST };
このファイルは、クライアントサイドからのtRPCリクエストを受け取り、適切なtRPCルーターに渡す役割を果たしています。また、リクエストのコンテキストを作成し、エラーハンドリングを行います。
Next.jsのApp RouterのRoute Handler機能を使用し、handler
関数をGET
とPOST
メソッドとしてエクスポートすることで、1つの関数で両方のHTTPメソッドを処理できるようにしています。
createContext
関数:
この関数は、各リクエストに対してtRPCコンテキストを作成します。ヘッダー情報を含むことで、認証情報などをtRPCプロシージャ内で利用できるようにしています。
fetchRequestHandler
関数:
この関数は、tRPCリクエストを処理する中心的な役割を果たします。エンドポイント、リクエスト、ルーター、コンテキスト作成関数を指定することで、適切なtRPCプロシージャにリクエストをルーティングします。
これで、Client ComponentsでtRPCを実行するための設定まで追うことができました。続いて、Sever Components / Client ComponentsでサンプルProcedureの呼び出しをどのように行っているか確認していきます。
どのコンポーネントでtRPCを呼び出しているか
tRPCリクエストを実行している箇所を確認していく前に、どのコンポーネントでサンプルProcedureを呼び出しているか説明します。
「サンプルプロシージャの確認」で説明したように、hello
/ create
/ getLatest
Procedureが画面に実装されていました。これらのProcedureは、アプリケーション内の異なるコンポーネントで使用されています。
ファイル名 | コンポーネントタイプ | 使用Procedure |
---|---|---|
src/app/page.tsx |
ServerComponents |
hello / getLatest
|
src/app/_components/post.tsx |
ClientComponents |
create / getLatest
|
getLatest
Procedureは、両方のファイルで使用されています。注目しておきたいのは、src/app/page.tsx
はsrc/app/_components/post.tsx
の親コンポーネントであり、キャッシュしたデータを受け渡す処理がなされている点です。
では、具体的なコードを確認していきましょう。
src/app/page.tsx
でhello
とgetLatest
を呼び出す
ServerComponentsであるimport Link from "next/link";
import { LatestPost } from "~/app/_components/post";
import { getServerAuthSession } from "~/server/auth";
import { api, HydrateClient } from "~/trpc/server";
export default async function Home() {
// サーバーサイドでtRPCクエリを実行
const hello = await api.post.hello({ text: "from tRPC" });
// サーバーサイドで認証セッションを取得
const session = await getServerAuthSession();
// 最新の投稿データをプリフェッチ(voidはこの結果を無視することを示す)
void api.post.getLatest.prefetch();
return (
// サーバーサイドでフェッチされたデータをクライアントサイドへ渡すためのHydrateClient
<HydrateClient>
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
{/* 略 */}
<p className="text-2xl text-white">
{hello ? hello.greeting : "Loading tRPC query..."}
</p>
{/* 略 */}
{/* ユーザーがログインしている場合のみ最新の投稿を表示 */}
{session?.user && <LatestPost />}
</div>
</main>
</HydrateClient>
);
}
このファイルはアプリケーションのルートページコンポーネントであり、http://localhost:3000/ にアクセスした際に表示される画面です。Next.jsのApp Routerでは、すべてのコンポーネントがデフォルトでServerComponentとして扱われ、このファイルもServerComponentになります。
ServerComponentsでは、src/trpc/server.ts
で設定したヘルパー関数であるapi
を使うだけでtRPCを呼び出すことができます。
hello
Procedureの呼び出し:
このprocedureは{ text: string }
を入力として受け取り、{ greeting: string }
を返します。Server Componentであるpage.tsx
内ではこのように直接await
を使用してこのprocedureを呼び出し、結果を取得できます。
getLatest
Procedureの呼び出し:
このProcedureは最新の投稿を取得します。 void api.post.getLatest.prefetch();
のprefetch()
は、tRPCのヘルパーメソッドです。クライアントサイドで使うuseQuery や useMutationのようなものです。
prefetch はfetchと違って結果をスローしません。 その代わりに、prefetch はクエリをキャッシュに追加し、それをハイドレートしてクライアントに送信します。
void
は、この操作の結果を無視することを示しています(prefetchは副作用として扱われます)。
<HydrateClient>
の役割と子コンポーネントの<LatestPost />
への影響:
<HydrateClient>
コンポーネントは、サーバーサイドでフェッチされたデータ(この場合はgetLatest
のデータを含む)をクライアントサイドのTanstack Queryキャッシュに渡します。
上記のvoid api.post.getLatest.prefetch();
でキャッシュされたデータはHydrateClient
コンポーネントを使って、子コンポーネントの<LatestPost />
へ渡します。
これにより、LatestPost
コンポーネントが後でこのデータを使用する際に、すでにキャッシュされたデータを利用できるため、パフォーマンスが向上します。
最後に、子コンポーネントの<LatestPost />
が定義されているsrc/app/_components/post.tsx
でClientComponentsでのtRPCの実行方法を確認します.
src/app/_components/post.tsx
でgetLatest
とcreate
を呼び出す
ClientComponentsである"use client";
import { useState } from "react";
import { api } from "~/trpc/react";
export function LatestPost() {
// 最新の投稿を取得するクエリを実行
// Suspenseを使用してデータ取得中の状態を処理
const [latestPost] = api.post.getLatest.useSuspenseQuery();
// tRPCのユーティリティフックを取得
const utils = api.useUtils();
// 新しい投稿のタイトルを管理するためのstate
const [name, setName] = useState("");
// 新しい投稿を作成するためのミューテーションを設定
const createPost = api.post.create.useMutation({
onSuccess: async () => {
// 投稿作成成功後、投稿リストを再取得
await utils.post.invalidate();
// 入力フィールドをクリア
setName("");
},
});
return (
<div className="w-full max-w-xs">
{/* 最新の投稿があれば表示、なければメッセージを表示 */}
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
)}
{/* 新しい投稿を作成するフォーム */}
<form
onSubmit={(e) => {
e.preventDefault();
// フォーム送信時に新しい投稿を作成
createPost.mutate({ name });
}}
className="flex flex-col gap-2"
>
{/* 投稿タイトル入力フィールド */}
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full px-4 py-2 text-black"
/>
{/* 送信ボタン */}
<button
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
);
}
このファイルはLatestPost
コンポーネントを定義しており、最新の投稿を表示し、新しい投稿を作成するためのフォームを提供します。ファイルの先頭に"use client"
ディレクティブがあることから、これはClientComponentであることがわかります。
getLatest
Procedureの呼び出し:
このProcedureは最新の投稿を取得します。親コンポーネントであるpage.tsx
で使用されているHydrateClient
コンポーネントを通じて、このデータがすでにキャッシュに存在するため、追加のネットワークリクエストなしで即座にデータを表示できます。useSuspenseQuery
フックを使用しており、React Suspenseの機能を利用してデータ取得中に自動的にコンポーネントをサスペンドします。
create
Procedureの呼び出し:
このProcedureは新しい投稿を作成します。データの変更操作を行うためのuseMutation
フックを使用しています。onSuccess
コールバック内でutils.post.invalidate()
投稿関連のクエリキャッシュを無効化し、最新のデータを再取得します。
送信ボタンの制御:
createPost.isPending
プロパティを使用して、ミューテーション実行中はボタンを無効化しています。これにより、ユーザーが処理中に複数回ボタンをクリックすることを防ぎ、重複した投稿を防止します。
まとめ
本記事では、create-t3-appで生成されたプロジェクトのコード解説を通じてtRPCの基本について解説しました。
create-t3-appの雛形のコードはシンプルですが、tRPCとTanstack Queryとの連携、Next.jsのServerComponentsやClientComponentsでのデータキャッシュの扱いまで網羅的にたどることができました。この記事がtRPCの理解の一助になれば嬉しいです。
最後までお読みいただき、ありがとうございました。
Discussion