PrismaスキーマをClaude Codeに渡せば、Zodバリデーター・Next.js Route Handlers・tRPCルーターを型安全に一括生成できる。1モデルあたり1時間かかっていたCRUD実装が10分で終わる。
この記事では、Prismaスキーマを起点にAPI一式を自動生成するワークフローを、具体的なプロンプトとともに解説する。実際のプロジェクトで使えるフローで、手書きに戻れなくなるはずだ。
PrismaスキーマからAPIを生成するワークフローの全体像
まず全体の流れを把握しておく。
ポイントは、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 に追加する:
{
"mcpServers": {
"prisma": {
"command": "npx",
"args": [
"-y",
"prisma",
"mcp"
]
}
}
}
-y フラグを付け忘れるとClaude Codeが応答しなくなることがある。npmのインストール確認プロンプトで止まっているだけなので、忘れずに付けよう。
MCP Serverが使えない環境では、スキーマファイルを直接指定する方法でも十分動く:
claude "prisma/schema.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が生成するコードの例:
// 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>
生成後は型の整合性を確認する:
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のディレクトリ構造に従う
生成されるコードの例:
// 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を使って適切なコードで返す
生成されるルーターの例:
// 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のラッパー関数にまとめる
// 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 に配置
生成されるテストのスケルトン例:
// __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エラーが返るバグの原因になる。
スキーマ変更後のチェックリスト:
prisma migrate dev— DBにマイグレーション適用 + Prisma Client再生成- Claude Codeに変更差分を伝えてAPI・バリデーター・テストを更新
npx tsc --noEmit— 型エラーがないか確認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分で同等のコードが揃う。型安全性はむしろ上がる。まずはバリデーターから生成して、そこから外側に広げていこう。
関連記事: