32blogby StudioMitsu
claude-code10 min read

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.

claude-codeprismaAPI generationtRPCNext.jsTypeScript
On this page

Every time I added a new model to my Prisma schema, writing the CRUD API by hand was a grind. Once I started handing the schema to Claude Code, it would generate Route Handlers, Zod validators, and the rest of the stack in one shot.

This article walks through a full workflow for generating Zod validators, Next.js App Router Route Handlers, and tRPC routers from a Prisma schema — with specific prompts at each step, and with type safety throughout.

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

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

prisma/schema.prisma
        ↓
 Claude Code reads and understands it
        ↓
┌──────────────────────────────┐
│ Zod validators               │  lib/validations/
│ Route Handlers               │  app/api/**
│ tRPC routers                 │  server/routers/
│ Auth middleware              │  middleware/
│ Tests                        │  __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 + 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.

bash
npm install -D @prisma/mcp-server

Add it to ~/.claude.json:

json
{
  "mcpServers": {
    "prisma": {
      "command": "npx",
      "args": [
        "-y",
        "@prisma/mcp-server",
        "--schema", "./prisma/schema.prisma"
      ]
    }
  }
}

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.

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 would normally take an hour per model is done in minutes.

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:

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.

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 vitest-mock-extended
- 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, beforeEach } 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.

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

  1. prisma migrate dev — apply migration to database
  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
  5. prisma generate — regenerate Prisma Client

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.