tRPC + Zod でフロントとバックエンドの型を共有する

スポンサーリンク

tRPC + Zod でフロントとバックエンドの型を共有する

tRPC は TypeScript のフルスタックフレームワークで、Zod でAPIの入出力スキーマを定義するとフロントエンドに型が自動で伝わります。APIクライアントを手書きせず、型安全なAPI呼び出しができます。


早わかりまとめ

役割 担当
API ルートの定義 tRPC router(サーバー)
入力のバリデーション Zod スキーマ
型の共有 TypeScript 推論(自動)
クライアントの型補完 tRPC クライアントが自動で解決

インストール

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod

サーバー側:router を定義する

// server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

export const appRouter = t.router({
  // ユーザー取得
  getUser: t.procedure
    .input(z.object({ id: z.number() }))
    .query(({ input }) => {
      // input.id は number 型が保証されている
      return { id: input.id, name: 'Alice', email: 'alice@example.com' }
    }),

  // ユーザー作成
  createUser: t.procedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }))
    .mutation(({ input }) => {
      // input は { name: string; email: string } 型
      return { id: 1, ...input }
    }),
})

export type AppRouter = typeof appRouter

クライアント側:型補完が効く状態で呼び出す

// client.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from './server/router'

export const trpc = createTRPCReact<AppRouter>()
// components/UserProfile.tsx
import { trpc } from '@/client'

const UserProfile = ({ userId }: { userId: number }) => {
  // input の型は { id: number } と推論される
  const { data } = trpc.getUser.useQuery({ id: userId })

  return <div>{data?.name}</div>
}
// components/CreateUserForm.tsx
import { trpc } from '@/client'

const CreateUserForm = () => {
  const createUser = trpc.createUser.useMutation()

  const handleSubmit = () => {
    // 引数の型が自動で { name: string; email: string } になる
    createUser.mutate({ name: 'Bob', email: 'bob@example.com' })
  }

  return <button onClick={handleSubmit}>作成</button>
}

型がどう共有されるか

サーバー(router)
  └─ Zod スキーマで input/output を定義
       └─ AppRouter 型としてエクスポート
            └─ クライアントに型だけ渡す(コードは渡さない)
                 └─ useQuery / useMutation で型補完が効く

実行時にAPIを叩くのは HTTP ですが、TypeScript の型レベルでは直接繋がっているように見えます。


バリデーションエラーはクライアントに伝わる

// サーバー側スキーマ
.input(z.object({ email: z.string().email() }))

// クライアント側
createUser.mutate({ email: 'invalid' })
// → TRPCError として返ってくる
// → error.data?.zodError でフィールドごとのエラーが取れる

if (createUser.error?.data?.zodError) {
  const fieldErrors = createUser.error.data.zodError.fieldErrors
  console.log(fieldErrors.email) // ['Invalid email']
}

まとめ

やること コード
router の定義 t.router({ ... })
入力スキーマ .input(z.object({ ... }))
query(取得) .query(({ input }) => ...)
mutation(変更) .mutation(({ input }) => ...)
型のエクスポート export type AppRouter = typeof appRouter
クライアント生成 createTRPCReact<AppRouter>()

Zod の基本的なスキーマ定義については「Zod 入門:z.object・z.infer・safeParse の使い方」を参照してください。