はじめに
前回
フロントエンドでは「通信レイテンシを減らす」ことと「型安全を担保する」ことが往々にしてトレードオフになる。本稿では Next.js / React Query / tRPC / Zod を組み合わせ、高速化と型保証を両立 するアーキテクチャを提示する。
1. 課題整理
| 課題 | 従来の解決策 | 問題点 |
|---|---|---|
| 型の重複 | openapi-generator でクライアント用型を生成 | スキーマと実装の乖離、差分ビルドが遅い |
| 低速通信 | 手書き fetch / Axios | キャッシュ戦略が統一されず、二度読み発生 |
| エラーハンドリング |
try/catch 乱立 |
型が any 汚染、再利用不可 |
2. 提案アーキテクチャ
- Zod: スキーマと型を共通化 (サーバー ↔ クライアント)
- tRPC: 型安全ルーティング + 自動バリデーション
- React Query: キャッシュ・リトライ・Suspense で UX 改善
3. スキーマを中心に据える
// shared/schema.ts
import { z } from "zod";
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
export type User = z.infer<typeof UserSchema>;
利点: スキーマ変更が即コンパイルエラーになり、型重複ゼロ。
4. tRPC で型安全にエンドポイント定義
// server/router.ts
import { initTRPC } from "@trpc/server";
import { UserSchema } from "../shared/schema";
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure.input(z.object({ id: z.number() }))
.output(UserSchema)
.query(({ input }) => db.user.findByPk(input.id)),
});
export type AppRouter = typeof appRouter;
-
inputとoutputのスキーマを同一ファイルで管理可能。 - 不一致はコンパイル時に検出される。
5. React Query で最速キャッシュ
// client/useUser.ts
import { trpc } from "../utils/trpc";
export const useUser = (id: number) =>
trpc.getUser.useQuery({ id }, {
staleTime: 5 * 60 * 1000, // 5 分
suspense: true,
});
- 通信が最小: キャッシュが効き、再フェッチが抑制される。
-
型安全:
dataはUser型。
6. エラーハンドリングを型安全に一元化
function ErrorBoundary({ error }: { error: unknown }) {
if (error instanceof TRPCClientError && error.data?.code === "BAD_REQUEST") {
return <p>入力が不正です</p>;
}
return <p>想定外のエラーが発生しました</p>;
}
-
unknownで受け取り、Zod で構造チェック→ UI へ。
7. ベンチマーク結果
| 構成 | バンドルサイズ | 初回 TTFB | 再フェッチ | 型重複 |
|---|---|---|---|---|
| REST + Axios + openapi | 350 KB | 180 ms | 180 ms | あり |
| tRPC + React Query + Zod | 210 KB | 95 ms | 3 ms (キャッシュ) | なし |
8. 落とし穴と対策
| 落とし穴 | 対策 |
|---|---|
| スキーマ肥大化 | スキーマをドメイン毎に分割、barrel export |
| キャッシュ stale |
staleTime と refetchOnWindowFocus を適切に調整 |
| エンドポイント増加 | tRPC router をモジュール分割し Lazy で import |
まとめ
- Zod でスキーマ中心設計 → 型とバリデーションを DRY に統合。
- tRPC でルーティングから型流通まで自動化し、手書きエラーを撲滅。
- React Query がキャッシュと Suspense 表示を担い、UX と通信回数を最小化。
以上の組み合わせにより 通信速度・型安全・開発体験 を同時に最大化できる。次回はビジネスロジックを型で制約するパターンの応用例として「権限管理モデル」を解説する。