記事

tRPCとReactとHonoではじめる個人開発テンプレート

tRPC
React
Hono
2. アーキテクチャ全体像

┌─────────────────────────────────────────────────────────┐
│  Frontend (React)                                       │
│  ┌───────────────────────────────────────────────────┐  │
│  │  trpc.useQuery() / useMutation()                  │  │
│  │         ↓                                         │  │
│  │  @trpc/react-query (TanStack Query wrapper)       │  │
│  │         ↓                                         │  │
│  │  @trpc/client (HTTP通信)                          │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          │
                          │ HTTP POST (全部POSTで送る)
                          ↓
┌─────────────────────────────────────────────────────────┐
│  Backend (Hono)                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │  @hono/trpc-server (アダプター)                    │  │
│  │         ↓                                         │  │
│  │  @trpc/server                                     │  │
│  │         ↓                                         │  │
│  │  Router → Procedures (実際のロジック)              │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

3. 構成要素の役割

サーバー側

// ① 基盤となるtRPCインスタンス生成
import { initTRPC } from '@trpc/server';

const t = initTRPC.context<Context>().create();

// ② ビルディングブロック
export const router = t.router;           // ルーター作成用
export const publicProcedure = t.procedure; // 認証不要のprocedure
// ③ Router定義(procedureの集合体)
const userRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      return ctx.db.user.findUnique({ where: { id: input.id } });
    }),
});

// ④ ルーターの合成
const appRouter = router({
  user: userRouter,
  post: postRouter,
});

// ⑤ 型のエクスポート(これがキモ)
export type AppRouter = typeof appRouter;
// ⑥ Honoとの接続
import { Hono } from 'hono';
import { trpcServer } from '@hono/trpc-server';

const app = new Hono();

app.use('/trpc/*', trpcServer({
  router: appRouter,
  createContext: (opts) => createContext(opts), // 後述
}));

クライアント側

// ① クライアント作成
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 型だけimport

export const trpc = createTRPCReact<AppRouter>();
// ② Providerセットアップ
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';

const queryClient = new QueryClient();
const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});

function App() {
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {/* アプリ本体 */}
      </QueryClientProvider>
    </trpc.Provider>
  );
}
// ③ 実際の使用
function UserProfile({ userId }: { userId: string }) {
  // 型が自動推論される
  const { data, isLoading } = trpc.user.getById.useQuery({ id: userId });
  
  if (isLoading) return <div>Loading...</div>;
  return <div>{data?.name}</div>; // dataの型も推論済み
}

4. 型共有の仕組み(重要)

サーバー側で定義:
  appRouter → typeof appRouter → AppRouter型

クライアント側で使用:
  import type { AppRouter } from '...'
  createTRPCReact<AppRouter>()

ここが誤解されやすい点:

  • 実行時にはサーバーのコードはクライアントに含まれない
  • import type はTypeScriptの型情報だけを取り込む
  • ビルド後のJSには何も残らない

つまり、モノレポ構成か、型定義だけをパッケージとして共有する必要がある。

5. Context(認証・DB接続など)

// Context型定義
interface Context {
  db: PrismaClient;
  user: User | null;
}

// Context生成関数
export const createContext = async (opts: { req: Request }) => {
  const token = opts.req.headers.get('Authorization');
  const user = token ? await verifyToken(token) : null;
  
  return {
    db: prisma,
    user,
  };
};
// 認証付きprocedureの作成
const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      ...ctx,
      user: ctx.user, // 型がnon-nullableになる
    },
  });
});

6. プロダクション導入時の考慮点

項目考慮事項
モノレポ構成型共有のため、turborepo/nx等で管理するのが現実的
エラーハンドリングTRPCErrorをどこでcatchするか設計
バリデーションZodスキーマの再利用戦略
キャッシュTanStack Queryのキャッシュ設計
認証middleware chainでの認証処理
ログ・監視procedureのmiddlewareでログ仕込む

7. 典型的なディレクトリ構成

monorepo/
├── apps/
│   ├── web/                 # Reactフロントエンド
│   │   ├── src/
│   │   │   ├── trpc.ts      # クライアント設定
│   │   │   └── ...
│   │   └── package.json
│   │
│   └── api/                 # Honoバックエンド
│       ├── src/
│       │   ├── trpc/
│       │   │   ├── index.ts     # tRPC初期化
│       │   │   ├── context.ts   # Context定義
│       │   │   └── routers/
│       │   │       ├── user.ts
│       │   │       ├── post.ts
│       │   │       └── index.ts # appRouter
│       │   └── index.ts     # Honoエントリ
│       └── package.json
│
└── packages/
    └── shared/              # 共有Zodスキーマ等(任意)

© 2026 Shotaro Kinjo