32blogby Studio Mitsu

Claude Code × Prisma — スキーマからAPIを自動生成する

PrismaスキーマをClaude Codeに渡すだけでRESTエンドポイント・tRPCルーター・Zodバリデーターを自動生成する手順。Next.js App RouterのRoute Handlersまで一気通貫で解説。

by omitsu18 min read
Claude CodePrismaAPI generationtRPCNext.jsTypeScript
目次

PrismaスキーマをClaude Codeに渡せば、Zodバリデーター・Next.js Route Handlers・tRPCルーターを型安全に一括生成できる。1モデルあたり1時間かかっていたCRUD実装が10分で終わる。

この記事では、Prismaスキーマを起点にAPI一式を自動生成するワークフローを、具体的なプロンプトとともに解説する。実際のプロジェクトで使えるフローで、手書きに戻れなくなるはずだ。

PrismaスキーマからAPIを生成するワークフローの全体像

まず全体の流れを把握しておく。

Prisma Schemaschema.prisma解析Claude Code読み込み → 生成自動生成API 一式Zod / Route / tRPC検証テスト__tests__/

ポイントは、Prismaスキーマを唯一の信頼できる情報源(Single Source of Truth)として扱う ことだ。スキーマが変われば、Claude Codeにdiffを渡すだけで下流のコードが連鎖的に更新される。

前提となる技術スタック:

  • Next.js 15+(App Router)
  • Prisma 6.x / 7.x + PostgreSQL
  • TypeScript 5.x
  • Zod 3.x
  • tRPC 11.x(tRPCを使わないプロジェクトは該当セクションをスキップ)

Prisma MCP Serverのセットアップ

Prisma公式のMCPサーバーを使うと、Claude Codeがスキーマだけでなくマイグレーション履歴やデータベースの状態も把握した上でコードを生成できる。

Prisma CLI v6.6.0以降はMCPサーバーが組み込み済みのため、別途パッケージをインストールする必要はない。

~/.claude.json に追加する:

json
{
  "mcpServers": {
    "prisma": {
      "command": "npx",
      "args": [
        "-y",
        "prisma",
        "mcp"
      ]
    }
  }
}

-y フラグを付け忘れるとClaude Codeが応答しなくなることがある。npmのインストール確認プロンプトで止まっているだけなので、忘れずに付けよう。

MCP Serverが使えない環境では、スキーマファイルを直接指定する方法でも十分動く:

bash
claude "prisma/schema.prisma を読んで、スキーマの構造を把握してください"

サンプルとして使うスキーマ:

prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String    @id @default(cuid())
  email     String    @unique
  name      String?
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Post {
  id          String    @id @default(cuid())
  title       String
  content     String?
  published   Boolean   @default(false)
  author      User      @relation(fields: [authorId], references: [id])
  authorId    String
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

enum Role {
  USER
  ADMIN
}

スキーマからZodバリデーターを自動生成する

まずZodバリデーターを生成する。後のRoute HandlerもtRPCルーターもここからインポートする。

prisma/schema.prisma のスキーマを読んで、以下のZodバリデーターを生成してください。

- User モデルの作成・更新・検索用バリデーター
- Post モデルの作成・更新・検索用バリデーター
- 出力先: lib/validations/ ディレクトリ(モデルごとに1ファイル)
- Prismaの型と整合性を取ること
- emailはZodのemail()バリデーションを使う
- Partialを使って更新用スキーマを派生させる
- 各スキーマからTypeScript型をexportする

Claude Codeが生成するコードの例:

typescript
// lib/validations/user.ts
import { z } from 'zod'
import { Role } from '@prisma/client'

export const createUserSchema = z.object({
  email: z.string().email('有効なメールアドレスを入力してください'),
  name: z.string().min(1).max(100).optional(),
  role: z.nativeEnum(Role).default('USER'),
})

export const updateUserSchema = createUserSchema.partial()

export const userQuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  role: z.nativeEnum(Role).optional(),
  search: z.string().optional(),
})

export type CreateUserInput = z.infer<typeof createUserSchema>
export type UpdateUserInput = z.infer<typeof updateUserSchema>
export type UserQuery = z.infer<typeof userQuerySchema>

生成後は型の整合性を確認する:

bash
npx tsc --noEmit

エラーが出たら、そのエラーメッセージをそのままClaude Codeに投げると修正してくれる。初回生成で型が正しいことがほとんどだが、optionalなリレーションフィールドや複合ユニーク制約まわりのエッジケースで修正が必要になることがある。

Next.js App RouterのRoute Handlersを自動生成する

Zodバリデーターが揃ったところで、Route Handlersを生成する。

lib/validations/user.ts と lib/validations/post.ts のバリデーターを使って、
以下のRoute Handlersを生成してください。

User API:
- GET /api/users — 一覧取得(ページネーション・検索対応)
- POST /api/users — 新規作成
- GET /api/users/[id] — 単一取得
- PATCH /api/users/[id] — 更新
- DELETE /api/users/[id] — 削除

Post API:
- 同様のCRUD

条件:
- Prisma Clientはlib/prisma.tsからインポート
- エラーはNextResponseで適切なHTTPステータスを返す
- バリデーションエラーは400、Not Foundは404を返す
- 各ファイルはApp Routerのディレクトリ構造に従う

生成されるコードの例:

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { createUserSchema, userQuerySchema } from '@/lib/validations/user'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const query = userQuerySchema.safeParse(Object.fromEntries(searchParams))

  if (!query.success) {
    return NextResponse.json(
      { error: 'Invalid query parameters', details: query.error.flatten() },
      { status: 400 }
    )
  }

  const { page, limit, role, search } = query.data
  const skip = (page - 1) * limit

  const where = {
    ...(role && { role }),
    ...(search && {
      OR: [
        { email: { contains: search, mode: 'insensitive' as const } },
        { name: { contains: search, mode: 'insensitive' as const } },
      ],
    }),
  }

  const [users, total] = await Promise.all([
    prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' } }),
    prisma.user.count({ where }),
  ])

  return NextResponse.json({
    data: users,
    pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
  })
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const parsed = createUserSchema.safeParse(body)

  if (!parsed.success) {
    return NextResponse.json(
      { error: 'Validation failed', details: parsed.error.flatten() },
      { status: 400 }
    )
  }

  const user = await prisma.user.create({ data: parsed.data })
  return NextResponse.json({ data: user }, { status: 201 })
}

ページネーション、バリデーション、エラーハンドリングまで一式生成される。手書きで1モデル1時間かかっていたのが10分足らずで完了する。深夜2時に手書きするより出力の一貫性も高い。

tRPCルーターをPrismaスキーマから生成する

tRPCを使っているプロジェクトでは、Route Handlers代わりにtRPCルーターを生成できる。

lib/validations/ のZodスキーマを使って、
以下のtRPCルーターを生成してください。

- server/routers/user.ts — User の CRUD procedures
- server/routers/post.ts — Post の CRUD procedures
- server/routers/index.ts — appRouter としてまとめる

条件:
- tRPC v11 の書式に従う
- protectedProcedure と publicProcedure を使い分ける
  (一覧取得はpublic、作成・更新・削除はprotected)
- input は既存のZodスキーマを再利用する
- エラーはTRPCErrorを使って適切なコードで返す

生成されるルーターの例:

typescript
// server/routers/user.ts
import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { createTRPCRouter, publicProcedure, protectedProcedure } from '@/server/trpc'
import { prisma } from '@/lib/prisma'
import { createUserSchema, updateUserSchema, userQuerySchema } from '@/lib/validations/user'

export const userRouter = createTRPCRouter({
  list: publicProcedure
    .input(userQuerySchema)
    .query(async ({ input }) => {
      const { page, limit, role, search } = input
      const skip = (page - 1) * limit
      const where = {
        ...(role && { role }),
        ...(search && {
          OR: [
            { email: { contains: search, mode: 'insensitive' as const } },
            { name: { contains: search, mode: 'insensitive' as const } },
          ],
        }),
      }
      const [users, total] = await Promise.all([
        prisma.user.findMany({ where, skip, take: limit }),
        prisma.user.count({ where }),
      ])
      return { users, total, totalPages: Math.ceil(total / limit) }
    }),

  create: protectedProcedure
    .input(createUserSchema)
    .mutation(async ({ input }) => {
      const existing = await prisma.user.findUnique({ where: { email: input.email } })
      if (existing) {
        throw new TRPCError({ code: 'CONFLICT', message: 'このメールアドレスは既に使用されています' })
      }
      return prisma.user.create({ data: input })
    }),

  update: protectedProcedure
    .input(z.object({ id: z.string(), data: updateUserSchema }))
    .mutation(async ({ input }) => {
      const user = await prisma.user.findUnique({ where: { id: input.id } })
      if (!user) throw new TRPCError({ code: 'NOT_FOUND' })
      return prisma.user.update({ where: { id: input.id }, data: input.data })
    }),

  delete: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input }) => {
      await prisma.user.delete({ where: { id: input.id } })
      return { success: true }
    }),
})

認証・認可のミドルウェアをClaude Codeで追加する

生成したAPIにAuth.js(NextAuth v5)の認証ガードを追加する。

生成されたRoute Handlersに認証・認可ミドルウェアを追加してください。

要件:
- NextAuth.js v5 のセッション確認
- ADMIN ロールが必要なエンドポイント: DELETE /api/users/[id]
- 認証必須のエンドポイント: POST, PATCH, DELETE 全般
- 認証エラーは401、認可エラーは403を返す
- 共通化できる部分はwithAuth / withAdminのラッパー関数にまとめる
typescript
// lib/auth-middleware.ts
import { auth } from '@/auth'
import { NextRequest, NextResponse } from 'next/server'

type Handler = (req: NextRequest, context: { params: Promise<Record<string, string>> }) => Promise<NextResponse>

export function withAuth(handler: Handler): Handler {
  return async (req, context) => {
    const session = await auth()
    if (!session?.user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    return handler(req, context)
  }
}

export function withAdmin(handler: Handler): Handler {
  return async (req, context) => {
    const session = await auth()
    if (!session?.user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
    if (session.user.role !== 'ADMIN') {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }
    return handler(req, context)
  }
}

DELETE Route Handlerに withAdmin(handler) を適用するだけでいい。注意点として、auth の設定でセッションのcallbackに role フィールドを含めておくこと。Claude Codeはミドルウェアを正しく生成するが、auth設定側で role が公開されていないとランタイムで undefined になる。

生成したコードのテストもClaude Codeで自動生成する

テストもClaude Codeに生成させる。

app/api/users/route.ts のRoute Handlersに対するVitestのテストを書いてください。

条件:
- Prisma Clientはvi.mockでモックする
- 正常系: 一覧取得・作成・更新・削除
- 異常系: バリデーションエラー・Not Found・認証エラー
- テストファイルは __tests__/api/users.test.ts に配置

生成されるテストのスケルトン例:

typescript
// __tests__/api/users.test.ts
import { describe, it, expect, vi } from 'vitest'
import { NextRequest } from 'next/server'
import { GET, POST } from '@/app/api/users/route'
import { prisma } from '@/lib/prisma'

vi.mock('@/lib/prisma', () => ({
  prisma: {
    user: {
      findMany: vi.fn(),
      count: vi.fn(),
      create: vi.fn(),
    },
  },
}))

describe('GET /api/users', () => {
  it('ユーザー一覧を返す', async () => {
    vi.mocked(prisma.user.findMany).mockResolvedValue([
      { id: '1', email: 'test@example.com', name: 'Test', role: 'USER', createdAt: new Date(), updatedAt: new Date() },
    ])
    vi.mocked(prisma.user.count).mockResolvedValue(1)

    const request = new NextRequest('http://localhost/api/users')
    const response = await GET(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data.data).toHaveLength(1)
    expect(data.pagination.total).toBe(1)
  })
})

Prismaスキーマ変更時のAPI更新手順

スキーマ変更後の更新プロンプトを標準化しておくと、チームで使う際に一貫性が保てる。

以下のスキーマ変更をAPIに反映させてください:

変更内容:
- Post モデルに tags: String[] フィールドを追加
- User モデルに avatar: String? フィールドを追加

更新が必要なファイル:
- lib/validations/user.ts, post.ts
- app/api/users/route.ts, app/api/posts/route.ts
- 対応するテストファイル

既存のAPIの挙動は変えないこと(新フィールドはオプション)。

このプロンプト形式をチームで標準化しておくと、「スキーマを変えたけどAPIの更新を忘れた」という事故がなくなる。バリデーターの更新を忘れると、createエンドポイントだけ400エラーが返るバグの原因になる。

スキーマ変更後のチェックリスト:

  1. prisma migrate dev — DBにマイグレーション適用 + Prisma Client再生成
  2. Claude Codeに変更差分を伝えてAPI・バリデーター・テストを更新
  3. npx tsc --noEmit — 型エラーがないか確認
  4. npm test — テストがパスするか確認

prisma migrate dev は自動で prisma generate も実行するため、別途generateする必要はない。

よくある質問

Prismaのマルチファイルスキーマ(prismaSchemaFolder)にも対応できる?

対応できる。Prisma 5.15で導入されたマルチファイルスキーマを使っている場合は、"prisma/schema/ ディレクトリのファイルをすべて読んでバリデーターを生成して" と指示すればいい。MCPサーバーもマルチファイルスキーマを自動で処理する。

PrismaではなくDrizzle ORMでも同じワークフローが使える?

使える。プロンプトの「Prismaスキーマ」を「Drizzleスキーマ」に置き換えて、import先を調整するだけだ。DrizzleのスキーマはTypeScriptファイルなので、MCPサーバーなしでもClaude Codeが直接読める。Zod-firstのアプローチは同じように機能する。

生成されたコードの精度はどのくらい?全部レビューが必要?

初回生成で正しいことがほとんどだが、optionalなリレーションフィールド、複合ユニーク制約、DB固有の型まわりのエッジケースで修正が必要になることがある。出荷前に必ず tsc --noEmit とテストスイートを通すこと。

20モデル以上の大規模スキーマでもスケールする?

する。ただし一度に全部ではなく、関連するモデルを3-5個ずつバッチで生成するのがコツ。出力が集中して読みやすくなる。大規模なスキーマ(25モデル以上)では、ドメイン別にバッチを組むのがおすすめだ: ユーザー/認証系、コンテンツ系、課金系など。

REST APIの代わりにGraphQLリゾルバーを生成できる?

できる。Route Handlerのプロンプトを、使っているGraphQLフレームワーク(Apollo Server、Pothos等)向けに書き換えるだけだ。Zodバリデーターはそのまま入力バリデーション層として使える。スキーマ駆動の原則は同じで、出力フォーマットが変わるだけ。

生成されたコードがプロジェクトの既存API規約と合わない場合は?

プロンプトに規約を含めるか、プロジェクトの CLAUDE.md に書いておくのがベスト。たとえば「すべてのエンドポイントは { data, meta } 形式で返す」や「lib/errorsのカスタムApiErrorクラスを使う」など。Claude Codeは CLAUDE.md を自動で読むので、そこに書いた規約はすべての生成に適用される。

生成したコードはコミットすべき?毎回再生成すべき?

コミットすべき。生成されたコードは本番コードとして扱うこと。毎回再生成すると、生成後に手動で加えた修正(エラーメッセージのカスタマイズ、ビジネスロジックの追加など)が失われる。

まとめ

PrismaスキーマをClaude Codeと組み合わせることで、API実装の大部分を自動化できる。

重要なポイント:

  • Prisma MCPサーバー を使うとClaude Codeがスキーマを直接参照できる
  • Zodバリデーターを先に生成 して、Route HandlerとtRPCで共通利用する
  • 認証ミドルウェア はラッパー関数でまとめ、Route Handlerに適用する
  • スキーマ変更時のプロンプト形式 を決めておくと更新漏れが防げる

手書きCRUDが1モデルあたり1時間かかっていたとすると、このワークフローでは10-15分で同等のコードが揃う。型安全性はむしろ上がる。まずはバリデーターから生成して、そこから外側に広げていこう。

関連記事: