tRPCとReactとHonoではじめる個人開発テンプレート
public.icon
tRPC
React
Hono
2. アーキテクチャ全体像
code:_
┌─────────────────────────────────────────────────────────┐
│ 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. 構成要素の役割
サーバー側
code:typescript
// ① 基盤となるtRPCインスタンス生成
import { initTRPC } from '@trpc/server';
const t = initTRPC.context<Context>().create();
// ② ビルディングブロック
export const router = t.router; // ルーター作成用
export const publicProcedure = t.procedure; // 認証不要のprocedure
code:typescript
// ③ 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;
code:typescript
// ⑥ 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), // 後述
}));
クライアント側
code:typescript
// ① クライアント作成
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 型だけimport
export const trpc = createTRPCReact<AppRouter>();
code:typescript
// ② 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>
);
}
code:typescript
// ③ 実際の使用
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. 型共有の仕組み(重要)
code:_
サーバー側で定義:
appRouter → typeof appRouter → AppRouter型
クライアント側で使用:
import type { AppRouter } from '...'
createTRPCReact<AppRouter>()
ここが誤解されやすい点:
実行時にはサーバーのコードはクライアントに含まれない
import type はTypeScriptの型情報だけを取り込む
ビルド後のJSには何も残らない
つまり、モノレポ構成か、型定義だけをパッケージとして共有する必要がある。
5. Context(認証・DB接続など)
code:typescript
// 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,
};
};
code:typescript
// 認証付き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. プロダクション導入時の考慮点
table:_
項目 考慮事項
モノレポ構成 型共有のため、turborepo/nx等で管理するのが現実的
エラーハンドリング TRPCErrorをどこでcatchするか設計
バリデーション Zodスキーマの再利用戦略
キャッシュ TanStack Queryのキャッシュ設計
認証 middleware chainでの認証処理
ログ・監視 procedureのmiddlewareでログ仕込む
7. 典型的なディレクトリ構成
code:_
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スキーマ等(任意)