Pásale tu esquema Prisma a Claude Code y genera validadores Zod, Route Handlers de Next.js y routers tRPC — todo con seguridad de tipos. Un prompt reemplaza una hora de boilerplate CRUD.
Este artículo recorre un flujo de trabajo completo para generar toda la capa de API desde un esquema Prisma, con prompts específicos en cada paso. Este flujo funciona bien en proyectos reales y reduce significativamente el tiempo de implementación de APIs.
¿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:
El principio clave es tratar el esquema Prisma como la única fuente de verdad. Cuando el esquema cambia, le pasas el diff a Claude Code 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 / 7.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:
{
"mcpServers": {
"prisma": {
"command": "npx",
"args": [
"-y",
"prisma",
"mcp"
]
}
}
}
Si olvidas el flag -y, Claude Code se queda colgado esperando la confirmación de instalación de npm. Es fácil de pasar por alto pero fácil de corregir.
Si el servidor MCP no está disponible en tu entorno, apuntar a Claude Code directamente al archivo de esquema funciona bien:
claude "Lee prisma/schema.prisma y confirma que entiendes la estructura del modelo"
Aquí tienes el esquema de ejemplo que usaremos a lo largo del artículo:
// 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í.
Lee prisma/schema.prisma y genera validadores Zod para lo siguiente:
- Modelo User: esquemas de creación, actualización y consulta
- Modelo Post: esquemas de creación, actualización y consulta
Requisitos:
- Salida en el directorio lib/validations/, un archivo por modelo (user.ts, post.ts)
- Mantener los tipos consistentes con los tipos generados por Prisma
- Usar z.string().email() para campos de email
- Derivar esquemas de actualización desde los de creación usando .partial()
- Exportar tipos TypeScript inferidos de cada esquema
El resultado generado debería verse algo así:
// lib/validations/user.ts
import { z } from 'zod'
import { Role } from '@prisma/client'
export const createUserSchema = z.object({
email: z.string().email('Ingresa un correo electrónico válido'),
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:
npx tsc --noEmit
Si hay errores, pégalos directamente de vuelta a Claude Code y los corregirá. La primera generación suele acertar los tipos, pero los casos extremos con relaciones opcionales o restricciones únicas compuestas pueden necesitar correcciones manuales.
¿Cómo Auto-Generas Route Handlers de Next.js App Router?
Con los validadores en su lugar, genera los Route Handlers:
Usando los validadores en lib/validations/user.ts y lib/validations/post.ts,
genera Route Handlers para los siguientes endpoints:
User API:
- GET /api/users — listado con paginación y búsqueda
- POST /api/users — crear
- GET /api/users/[id] — obtener uno
- PATCH /api/users/[id] — actualizar
- DELETE /api/users/[id] — eliminar
Post API:
- Mismo patrón CRUD
Requisitos:
- Importar Prisma Client desde lib/prisma.ts
- Devolver códigos HTTP apropiados (400 para validación, 404 para no encontrado)
- Usar NextResponse para todas las respuestas
- Seguir las convenciones de archivos/directorios de App Router
Así se ve el endpoint de listado generado:
// 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 antes me tomaba una hora por modelo se hace en menos de 10 minutos, y la salida es más consistente que lo que yo escribiría a mano a las 2 de la madrugada.
¿Cómo Generas Routers tRPC desde el Esquema Prisma?
Para proyectos tRPC, el mismo enfoque genera routers en lugar de Route Handlers:
Usando los esquemas Zod en lib/validations/, genera routers tRPC:
- server/routers/user.ts — procedimientos CRUD de User
- server/routers/post.ts — procedimientos CRUD de Post
- server/routers/index.ts — combinar en appRouter
Requisitos:
- Usar sintaxis tRPC v11
- publicProcedure para operaciones de lectura, protectedProcedure para escritura
- Reutilizar los esquemas Zod existentes para validación de entrada
- Usar TRPCError con códigos apropiados para errores
El router generado:
// 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: 'El email ya está en uso' })
}
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 usando Auth.js (NextAuth v5):
Añade middleware de autenticación y autorización a los Route Handlers generados.
Requisitos:
- Usar verificación de sesión de NextAuth.js v5
- Requerir rol ADMIN para: DELETE /api/users/[id]
- Requerir autenticación para: todos los endpoints POST, PATCH, DELETE
- Devolver 401 para solicitudes no autenticadas, 403 para no autorizadas
- Crear funciones wrapper reutilizables withAuth y 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)
}
}
Aplicar withAdmin(handler) al Route Handler de DELETE es todo lo que se necesita. Un detalle que al principio se me pasó: asegúrate de que tu configuración de auth incluya el campo role en el callback de sesión — Claude Code genera el middleware correctamente, pero si tu config de auth no expone role en la sesión, obtendrás checks de undefined en tiempo de ejecución.
¿Puedes Automatizar la Generación de Tests para el Código de API Generado?
Los tests se generan de la misma manera:
Escribe tests de Vitest para los Route Handlers en app/api/users/route.ts.
Requisitos:
- Mockear Prisma Client usando vi.mock
- Cubrir: listar, crear, actualizar, eliminar (happy path)
- Cubrir: errores de validación, no encontrado, errores de auth
- Colocar tests en __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('devuelve una lista paginada de usuarios', 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:
Aplica los siguientes cambios de esquema a la capa de API:
Cambios:
- Se añadió tags: String[] al modelo Post
- Se añadió avatar: String? al modelo User
Archivos a actualizar:
- lib/validations/user.ts, post.ts
- app/api/users/route.ts, app/api/posts/route.ts
- Archivos de test correspondientes
Restricción: no cambiar el comportamiento existente de la API — los nuevos campos son opcionales.
Usar una estructura de prompt consistente como esta elimina el bug de "actualicé el esquema pero olvidé actualizar la API". Saltarse la actualización de un validador es una causa común de errores 400 que solo aparecen en el endpoint de creación.
Lista de verificación para ejecutar después de cada cambio de esquema:
prisma migrate dev— aplica la migración y regenera Prisma Client- Dale a Claude Code el diff del esquema, pídele que actualice validadores, handlers y tests
npx tsc --noEmit— confirma que no hay errores de tiponpm test— confirma que los tests pasan
Nota: prisma migrate dev ejecuta prisma generate automáticamente, así que no necesitas un paso separado de generate.
Preguntas Frecuentes
¿Claude Code funciona con el esquema multi-archivo de Prisma (prismaSchemaFolder)?
Sí. Si usas la funcionalidad de esquema multi-archivo introducida en Prisma 5.15, apunta a Claude Code al directorio: "Lee todos los archivos en prisma/schema/ y genera validadores". El servidor MCP también maneja esquemas multi-archivo automáticamente.
¿Puedo usar este flujo con Drizzle ORM en lugar de Prisma?
Los prompts se traducen directamente — cambia "esquema Prisma" por "esquema Drizzle" y ajusta las rutas de importación. La diferencia principal es que los esquemas de Drizzle son archivos TypeScript, así que Claude Code puede leerlos sin un servidor MCP. El enfoque Zod-first funciona de la misma manera.
¿Qué tan preciso es el código generado — necesito revisar todo?
El código CRUD generado suele ser correcto en la primera pasada. Los problemas restantes tienden a ser casos extremos: campos de relación opcionales, restricciones únicas compuestas o tipos específicos de la base de datos. Siempre ejecuta tsc --noEmit y tu suite de tests antes de enviar a producción.
¿Este enfoque escala para esquemas grandes con 20+ modelos?
Sí, pero genera en lotes en lugar de todo a la vez. Pídele a Claude Code que maneje 3-5 modelos relacionados por prompt. Esto mantiene la salida enfocada y hace la revisión manejable. Para esquemas grandes (25+ modelos), organizar los lotes por dominio funciona bien: modelos de usuario/auth, modelos de contenido, modelos de facturación, etc.
¿Claude Code puede generar resolvers GraphQL en lugar de endpoints REST?
Absolutamente. Reemplaza el prompt de Route Handler con uno dirigido a tu framework GraphQL (Apollo Server, Pothos, etc.). Los validadores Zod siguen sirviendo como la capa de validación de entrada. El principio schema-first es el mismo — solo cambia el formato de salida.
¿Qué pasa si el código generado entra en conflicto con las convenciones API existentes de mi proyecto?
Incluye tus convenciones en el prompt o, mejor aún, en el archivo CLAUDE.md de tu proyecto. Por ejemplo: "Todos los endpoints devuelven formato { data, meta }" o "Usar la clase custom ApiError de lib/errors". Claude Code lee CLAUDE.md automáticamente, así que las convenciones que pongas ahí se aplican a cada generación.
¿Debo hacer commit del código generado o regenerarlo cada vez?
Haz commit. El código generado es tu código de producción — trátalo como tal. Regenerar cada vez significaría perder cualquier ajuste manual que hayas hecho post-generación (mensajes de error personalizados, adiciones de lógica de negocio, etc.).
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í.
Artículos relacionados: