32blogby Studio Mitsu

Claude Code × Prisma: Auto-Generate APIs from Your Schema

Generate REST endpoints, tRPC routers, and Zod validators from Prisma schema using Claude Code. Full workflow for Next.js App Router with type safety.

by omitsu13 min read
Claude CodePrismaAPI generationtRPCNext.jsTypeScript
On this page

Hand Claude Code your Prisma schema, and it generates Zod validators, Next.js Route Handlers, and tRPC routers — with full type safety. One prompt replaces an hour of CRUD boilerplate.

This article walks through a complete workflow for generating an entire API layer from a Prisma schema, with specific prompts at each step. This flow works well on real projects and consistently cuts API implementation time significantly.

What Does the Full Schema-Driven API Generation Workflow Look Like?

Before diving in, here's the end-to-end flow:

Prisma Schemaschema.prismaParseClaude CodeRead → GenerateAuto-generateAPI StackZod / Routes / tRPCVerifyTests__tests__/

The key principle is treating the Prisma schema as the single source of truth. When the schema changes, you give Claude Code the diff and it updates everything downstream. No more hunting for every place a field is referenced.

Tech stack assumed throughout:

  • Next.js 15+ (App Router)
  • Prisma 6.x / 7.x + PostgreSQL
  • TypeScript 5.x
  • Zod 3.x
  • tRPC 11.x (skip that section if you're using REST only)

How Do You Set Up the Prisma MCP Server?

Prisma's official MCP server lets Claude Code read your schema, migration history, and database state directly — rather than relying on you to paste the schema into every prompt.

Prisma CLI v6.6.0 and later includes the MCP server built-in — no separate package install needed.

Add it to ~/.claude.json:

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

If you forget the -y flag, Claude Code hangs waiting for the install confirmation. It's just stuck on npm's prompt — easy to miss but easy to fix.

If the MCP server isn't available in your environment, pointing Claude Code at the schema file directly works fine:

bash
claude "Read prisma/schema.prisma and confirm you understand the model structure"

Here's the sample schema we'll use throughout:

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
}

How Do You Generate Zod Validators from the Prisma Schema?

Start with validators. Everything else will import from here.

Read prisma/schema.prisma and generate Zod validators for the following:

- User model: create, update, and query schemas
- Post model: create, update, and query schemas

Requirements:
- Output to lib/validations/ directory, one file per model (user.ts, post.ts)
- Keep types consistent with the Prisma-generated types
- Use z.string().email() for email fields
- Derive update schemas from create schemas using .partial()
- Export TypeScript types inferred from each schema

The generated output should look something like this:

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

export const createUserSchema = z.object({
  email: z.string().email('Please enter a valid email address'),
  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>

After generation, verify type correctness:

bash
npx tsc --noEmit

If there are errors, paste them directly back to Claude Code and it will fix them. The first generation usually gets types right, but edge cases with optional relations or composite unique constraints may need manual fixes.

How Do You Auto-Generate Next.js App Router Route Handlers?

With validators in place, generate the Route Handlers:

Using the validators in lib/validations/user.ts and lib/validations/post.ts,
generate Route Handlers for the following endpoints:

User API:
- GET /api/users — list with pagination and search
- POST /api/users — create
- GET /api/users/[id] — get single
- PATCH /api/users/[id] — update
- DELETE /api/users/[id] — delete

Post API:
- Same CRUD pattern

Requirements:
- Import Prisma Client from lib/prisma.ts
- Return appropriate HTTP status codes (400 for validation, 404 for not found)
- Use NextResponse for all responses
- Follow App Router file/directory conventions

Here's what the generated list endpoint looks like:

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 })
}

Pagination, validation, structured error responses — all generated. What used to take me about an hour per model is done in under 10 minutes, and the output is more consistent than what I'd write by hand at 2 AM.

How Do You Generate tRPC Routers from the Prisma Schema?

For tRPC projects, the same approach generates routers instead of Route Handlers:

Using the Zod schemas in lib/validations/, generate tRPC routers:

- server/routers/user.ts — User CRUD procedures
- server/routers/post.ts — Post CRUD procedures
- server/routers/index.ts — combine into appRouter

Requirements:
- Use tRPC v11 syntax
- publicProcedure for read operations, protectedProcedure for writes
- Reuse the existing Zod schemas for input validation
- Use TRPCError with appropriate codes for errors

The generated router:

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: 'Email already in use' })
      }
      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 }
    }),
})

How Do You Add Auth Middleware with Claude Code?

With the API structure in place, add authentication guards using Auth.js (NextAuth v5):

Add authentication and authorization middleware to the generated Route Handlers.

Requirements:
- Use NextAuth.js v5 session checking
- Require ADMIN role for: DELETE /api/users/[id]
- Require authentication for: all POST, PATCH, DELETE endpoints
- Return 401 for unauthenticated requests, 403 for unauthorized
- Create reusable withAuth and withAdmin wrapper functions
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)
  }
}

Applying withAdmin(handler) to the DELETE Route Handler is all it takes. One thing I initially overlooked: make sure your auth config includes the role field in the session callback — Claude Code generates the middleware correctly, but if your auth config doesn't expose role on the session, you'll get undefined checks at runtime.

Can You Automate Test Generation for the Generated API Code?

Tests get generated the same way:

Write Vitest tests for the Route Handlers in app/api/users/route.ts.

Requirements:
- Mock Prisma Client using vi.mock
- Cover: list, create, update, delete (happy path)
- Cover: validation errors, not found, auth errors
- Place tests in __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('returns a paginated user list', 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)
  })
})

How Do You Update the API Layer When the Prisma Schema Changes?

When the schema evolves, standardize the update prompt so nothing falls through the cracks:

Apply the following schema changes to the API layer:

Changes:
- Added tags: String[] to Post model
- Added avatar: String? to User model

Files to update:
- lib/validations/user.ts, post.ts
- app/api/users/route.ts, app/api/posts/route.ts
- Corresponding test files

Constraint: don't change existing API behavior — new fields are optional.

Using a consistent prompt structure like this eliminates the "I updated the schema but forgot to update the API" bug. Missing a validator update is a common cause of 400 errors that only show up on the create endpoint.

Here's a checklist to run after every schema change:

  1. prisma migrate dev — apply migration and regenerate Prisma Client
  2. Give Claude Code the schema diff, ask it to update validators, handlers, and tests
  3. npx tsc --noEmit — confirm no type errors
  4. npm test — confirm tests pass

Note: prisma migrate dev automatically runs prisma generate, so you don't need a separate generate step.

Frequently Asked Questions

Does Claude Code work with Prisma's multi-file schema (prismaSchemaFolder)?

Yes. If you're using the multi-file schema feature introduced in Prisma 5.15, point Claude Code at the directory: "Read all files in prisma/schema/ and generate validators". The MCP server also handles multi-file schemas automatically.

Can I use this workflow with Drizzle ORM instead of Prisma?

The prompts translate directly — swap "Prisma schema" for "Drizzle schema" and adjust import paths. The main difference is that Drizzle schemas are TypeScript files, so Claude Code can read them without an MCP server. The Zod-first approach works the same way.

How accurate is the generated code — do I need to review everything?

The generated CRUD code is usually correct on the first pass. The remaining issues tend to be edge cases: optional relation fields, composite unique constraints, or database-specific types. Always run tsc --noEmit and your test suite before shipping.

Does this approach scale for large schemas with 20+ models?

Yes, but generate in batches rather than all at once. Ask Claude Code to handle 3-5 related models per prompt. This keeps the output focused and makes review manageable. For large schemas (25+ models), batching by domain works well: user/auth models, content models, billing models, etc.

Can Claude Code generate GraphQL resolvers instead of REST endpoints?

Absolutely. Replace the Route Handler prompt with one targeting your GraphQL framework (Apollo Server, Pothos, etc.). The Zod validators still serve as the input validation layer. The schema-first principle is the same — only the output format changes.

What if the generated code conflicts with my existing API conventions?

Include your conventions in the prompt or, better yet, in your project's CLAUDE.md file. For example: "All endpoints return { data, meta } format" or "Use custom ApiError class from lib/errors". Claude Code reads CLAUDE.md automatically, so conventions you put there apply to every generation.

Should I commit the generated code or regenerate it each time?

Commit it. The generated code is your production code — treat it as such. Regenerating each time would mean losing any manual tweaks you've made post-generation (error messages, business logic additions, etc.).

Wrapping Up

Combining Prisma's schema-first approach with Claude Code's code generation creates a workflow where most of the API boilerplate writes itself.

Key points:

  • Prisma MCP server lets Claude Code reference the schema directly without repeated copy-pasting
  • Generate Zod validators first — Route Handlers and tRPC routers both import from them
  • Auth middleware as wrapper functions keeps Route Handlers clean and the auth logic centralized
  • Standardize the schema-change prompt so updates propagate consistently across validators, endpoints, and tests

If CRUD implementation was taking an hour per model, this workflow brings it down to 10–15 minutes — with better type safety than hand-written code. Start with the validators and build outward from there.

Related articles: