32blogby StudioMitsu

Claude Code × Prisma: Auto-Genera APIs desde Tu Esquema

Genera endpoints REST, routers tRPC y validadores Zod desde el esquema Prisma usando Claude Code. Flujo completo para Next.js App Router con seguridad de tipos.

11 min read
Claude CodePrismaAPI generationtRPCNext.jsTypeScript
Contenido

Cada vez que añadía un nuevo modelo a mi esquema Prisma, escribir la API CRUD a mano era una rutina agotadora. Una vez que empecé a pasarle el esquema a Claude Code, generaba Route Handlers, validadores Zod y el resto del stack de una sola vez.

Este artículo recorre un flujo de trabajo completo para generar validadores Zod, Route Handlers de Next.js App Router y routers tRPC desde un esquema Prisma — con prompts específicos en cada paso, y con seguridad de tipos en todo momento.

¿Cómo Es el Flujo Completo de Generación de API Basada en Esquema?

Antes de profundizar, aquí tienes el flujo de extremo a extremo:

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__/
└──────────────────────────────┘

El principio clave es tratar el esquema Prisma como la única fuente de verdad. Cuando el esquema cambia, le das a Claude Code el diff y actualiza todo aguas abajo. No más búsquedas de cada lugar donde se referencia un campo.

Stack tecnológico asumido a lo largo del artículo:

  • Next.js 15 (App Router)
  • Prisma 6.x + PostgreSQL
  • TypeScript 5.x
  • Zod 3.x
  • tRPC 11.x (salta esa sección si solo usas REST)

¿Cómo Configuras el Servidor MCP de Prisma?

El servidor MCP oficial de Prisma permite que Claude Code lea tu esquema, historial de migraciones y estado de la base de datos directamente — en lugar de depender de que pegues el esquema en cada prompt.

Prisma CLI v6.6.0 y versiones posteriores incluyen el servidor MCP integrado — no se necesita instalar un paquete aparte.

Añádelo a ~/.claude.json:

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

Si el servidor MCP no está disponible en tu entorno, apuntar a Claude Code directamente al archivo de esquema funciona bien:

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

Aquí tienes el esquema de ejemplo que usaremos a lo largo del artículo:

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
}

¿Cómo Generas Validadores Zod desde el Esquema Prisma?

Empieza con los validadores. Todo lo demás importará de aquí.

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

El resultado generado debería verse algo así:

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>

Después de la generación, verifica la corrección de tipos:

bash
npx tsc --noEmit

Si hay errores, pégalos directamente de vuelta a Claude Code y los corregirá.

¿Cómo Auto-Generas Route Handlers de Next.js App Router?

Con los validadores en su lugar, genera los 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

Así se ve el endpoint de listado generado:

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

Paginación, validación, respuestas de error estructuradas — todo generado. Lo que normalmente tomaría una hora por modelo se hace en minutos.

¿Cómo Generas Routers tRPC desde el Esquema Prisma?

Para proyectos tRPC, el mismo enfoque genera routers en lugar de 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

El router generado:

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

¿Cómo Añades Middleware de Autenticación con Claude Code?

Con la estructura de la API en su lugar, añade guardias de autenticación:

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

Aplicar withAdmin(handler) al Route Handler de DELETE es todo lo que se necesita.

¿Puedes Automatizar la Generación de Tests para el Código de API Generado?

Los tests se generan de la misma manera:

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

¿Cómo Actualizas la Capa de API Cuando Cambia el Esquema Prisma?

Cuando el esquema evoluciona, estandariza el prompt de actualización para que nada se escape:

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.

Usar una estructura de prompt consistente como esta elimina el bug de "actualicé el esquema pero olvidé actualizar la API."

Aquí tienes una lista de verificación para ejecutar después de cada cambio de esquema:

  1. prisma migrate dev — aplica la migración a la base de datos
  2. Dale a Claude Code el diff del esquema, pídele que actualice validadores, handlers y tests
  3. npx tsc --noEmit — confirma que no hay errores de tipo
  4. npm test — confirma que los tests pasan
  5. prisma generate — regenera el Prisma Client

Conclusión

Combinar el enfoque schema-first de Prisma con la generación de código de Claude Code crea un flujo de trabajo donde la mayor parte del boilerplate de la API se escribe solo.

Puntos clave:

  • El servidor MCP de Prisma permite que Claude Code haga referencia al esquema directamente sin copiar y pegar repetidamente
  • Genera los validadores Zod primero — los Route Handlers y routers tRPC importan de ellos
  • Middleware de autenticación como funciones wrapper mantiene los Route Handlers limpios y la lógica de auth centralizada
  • Estandariza el prompt de cambio de esquema para que las actualizaciones se propaguen consistentemente a través de validadores, endpoints y tests

Si la implementación CRUD te tomaba una hora por modelo, este flujo de trabajo lo reduce a 10–15 minutos — con mejor seguridad de tipos que el código escrito a mano. Empieza con los validadores y construye hacia afuera desde ahí.