Cambiar tema

Guía completa de Next.js + Prisma: de la configuración a la práctica (con solución a la fuga de conexiones)

Easton editorial illustration: cache waterfall instrument

La terminal vuelve a escupir esa línea roja que desespera: Error: Can't reach database server at localhost:5432. Bueno, más exactamente: FATAL: sorry, too many clients already.

La base de datos está encendida y la cadena de conexión parece correcta. ¿Por qué no conecta? Al arrancar el proyecto todo iba bien; tras cambiar el código unas veces empieza a fallar. ¿Reiniciar el servidor de desarrollo de Next.js? No sirve. ¿Reiniciar la base de datos? Se recupera un rato y vuelve a caerse.

Más tarde supe que es una de las trampas más clásicas de Next.js + Prisma en desarrollo: fuga de conexiones a la base de datos por hot reload. En pocas palabras, cada vez que cambias código, el hot reload de Next.js crea una nueva instancia de Prisma, pero las conexiones antiguas no se cierran solas y acaban reventando el pool.

Quizá te suene: quieres añadir una base de datos a tu proyecto Next.js y dudas entre Prisma, TypeORM y Drizzle; eliges Prisma, lo configuras y aparecen errores por todas partes; al diseñar el Schema no sabes cómo escribir uno a muchos o muchos a muchos; o, como me pasó a mí, a mitad del desarrollo la base de datos deja de responder de repente.

En realidad, la curva de aprendizaje de Prisma no es tan empinada y la configuración tampoco es tan complicada. Lo importante es dominar unos puntos clave: cómo montar el entorno, cómo evitar la fuga de conexiones, cómo diseñar el Schema y cómo escribir CRUD. Este artículo intenta ordenarte todo eso para que evites rodeos innecesarios.

¿Por qué elegir Prisma? (Comparación con TypeORM y SQL nativo)

En Next.js hay muchas opciones de base de datos. TypeORM es veterano y estable, Drizzle es ligero y nuevo, y el SQL nativo rinde al máximo. ¿Por qué usar Prisma entonces?

Seguridad de tipos, de verdad

La primera vez que vi los tipos TypeScript generados automáticamente por Prisma, me sorprendió de verdad. No un simple «está bien», sino un «vaya, ¿se puede hacer así?».

Escribes el Schema, ejecutas npx prisma generate y obtienes definiciones de tipos para todos los modelos. No interfaces básicas, sino tipos completos con autocompletado inteligente. Por ejemplo, al escribir prisma.user.findUnique({ where: { id: , VSCode te indica el tipo de id y qué campos puedes usar en where.

Comparación rápida:

  • TypeORM: decoradores manuales (@Entity(), @Column()…), tipos separados del schema de la base de datos, fácil desincronizar
  • SQL nativo: el resultado de la consulta es any, hay que escribir interfaces a mano y actualizar tipos cuando cambia la tabla
  • Prisma: el Schema es la única fuente de verdad; los tipos se sincronizan solos y basta con volver a generar tras cambiar el Schema

No es solo comodidad. La seguridad de tipos te permite detectar errores mientras escribes, no cuando la app revienta en runtime.

Experiencia de desarrollo: los detalles importan

Prisma tiene varios puntos que me resultan especialmente cómodos:

Prisma Studio: una interfaz visual para gestionar la base de datos. Con npx prisma studio puedes ver y editar datos en el navegador sin instalar TablePlus ni escribir SQL a mano.

Migraciones: prisma migrate dev genera y aplica migraciones con mucha menos fricción que TypeORM. Cambias el Schema, Prisma detecta diferencias y te pregunta si quieres generar la migración. Sin scripts SQL manuales ni líos de orden.

Sintaxis de consultas: la API de Prisma encaja muy bien con JavaScript:

const users = await prisma.user.findMany({
  where: { email: { contains: '@gmail.com' } },
  include: { posts: true },
  orderBy: { createdAt: 'desc' }
})

¿Intuitivo, no? Sin SQL ni decoradores: objetos y métodos normales.

Comparado con el QueryBuilder de TypeORM:

const users = await userRepository.createQueryBuilder("user")
  .where("user.email LIKE :email", { email: "%@gmail.com%" })
  .leftJoinAndSelect("user.posts", "posts")
  .orderBy("user.createdAt", "DESC")
  .getMany()

Misma funcionalidad, pero la sintaxis de Prisma es más clara y menos propensa a errores.

Rendimiento y ecosistema

Alguien dirá: «Los ORM rinden peor; en producción conviene SQL nativo».

Es un mito habitual. Prisma sí tiene cierto overhead, pero en la mayoría de proyectos es totalmente asumible. Además, Prisma optimiza varias cosas:

  • N+1 automático: con include, Prisma combina consultas y evita peticiones repetidas
  • Pool de conexiones: la configuración por defecto suele ser razonable (num_cpus * 2 + 1)
  • Consultas eficientes: solo trae los campos necesarios; con select reduces aún más el tráfico

¿Llegas a un cuello de botella? Prisma también admite SQL nativo:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}`

Comodidad del ORM y SQL nativo donde haga falta.

En ecosistema, Prisma supera 38k estrellas en GitHub, Vercel lo recomienda y la documentación es muy completa. La guía oficial de Next.js incluye integración con Prisma: es una opción mainstream.

No digo que Prisma sea perfecto, pero para la mayoría de proyectos full stack con Next.js sus ventajas pesan más que sus límites: tipado, experiencia de desarrollo y soporte del ecosistema.

Configurar el entorno: Prisma desde cero

Bien, eliges Prisma. El siguiente paso es montar el entorno. En diez minutos suele bastar.

Instalar dependencias e inicializar

Supongamos que ya tienes un proyecto Next.js; si no, ejecuta npx create-next-app@latest.

Instala Prisma:

npm install prisma @prisma/client

Dos paquetes:

  • prisma: CLI para inicializar, generar migraciones y abrir Studio
  • @prisma/client: cliente que consulta la base de datos

Después inicializa:

npx prisma init

Ese comando hace dos cosas:

  1. Crea prisma/schema.prisma (configuración central)
  2. Crea .env con la variable DATABASE_URL

Configurar la conexión a la base de datos

Abre .env y verás algo como:

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

Es un ejemplo para PostgreSQL. El formato es:

postgresql://usuario:contraseña@host:puerto/nombre_db?schema=public

Para desarrollo local, PostgreSQL es la opción recomendada: funciones completas, excelente soporte en Prisma y alta probabilidad de usarlo también en producción.

¿No tienes PostgreSQL? Docker es lo más rápido:

Crea docker-compose.yml:

version: '3.8'
services:
  postgres:
    image: postgres:15
    restart: always
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Levanta el servicio:

docker-compose up -d

Y ajusta .env:

DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"

Importante: no subas .env a Git. Asegúrate de que esté en .gitignore:

.env
.env.local

Si no, filtras credenciales.

Si usas MySQL o SQLite, la cadena cambia un poco:

MySQL:

DATABASE_URL="mysql://root:password@localhost:3306/mydb"

SQLite (archivo local, útil en proyectos pequeños):

DATABASE_URL="file:./dev.db"

Generar Prisma Client

Con la conexión lista, genera el cliente:

npx prisma generate

Lee schema.prisma, genera tipos TypeScript y métodos de consulta en node_modules/@prisma/client.

Cada vez que modifiques el Schema, vuelve a ejecutar npx prisma generate; si no, TypeScript se quejará de tipos desactualizados.

Hasta aquí, entorno listo: instalar → inicializar → configurar base de datos → generar cliente. Nada dramático.

El siguiente paso es resolver la fuga de conexiones por hot reload. Es clave y donde mucha gente se hunde.

Resolver la fuga de conexiones por hot reload (capítulo clave)

Ahora sí: el error que hace llorar a medio mundo, FATAL: sorry, too many clients already.

¿Qué está pasando?

En desarrollo, Next.js usa hot reload (Hot Module Replacement, HMR). Cambias código, guardas y la página se refresca sin reiniciar el servidor. Muy cómodo.

Pero con Prisma aparece el problema.

Cada hot reload recarga módulos. Si en una API route o Server Component haces new PrismaClient() directamente, se crea una instancia nueva.

Cada instancia abre conexiones nuevas a la base de datos. Las antiguas no se cierran solas: siguen colgadas.

PostgreSQL admite por defecto unas 100 conexiones (configurable, pero suele bastar). Tras varios cambios de código subes: 10, 20, 50, 100. Lleno. La base rechaza conexiones nuevas con too many clients already.

La primera vez no entendí nada: la base estaba bien, ¿por qué de repente no conectaba? Reiniciar el servidor dev ayudaba un rato y volvía a explotar. En GitHub Issues de Prisma, el #10247 está lleno de gente con el mismo dolor.

Patrón singleton: solución en una jugada

La solución es simple: un solo PrismaClient en toda la aplicación.

Guarda la instancia en globalThis. Ese objeto global no se vacía en hot reload. La primera vez creas la instancia; después la reutilizas y no abres conexiones nuevas.

Crea lib/prisma.ts en la raíz del proyecto:

import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma || new PrismaClient({
  log: ['query', 'error', 'warn'], // en desarrollo puedes ver todas las consultas
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Explicación breve:

  1. globalForPrisma tipa globalThis para TypeScript
  2. export const prisma = globalForPrisma.prisma || new PrismaClient() es el núcleo: reutiliza si ya existe; si no, crea una nueva
  3. if (process.env.NODE_ENV !== 'production') guarda la instancia en globalThis solo en desarrollo. En producción no hace falta: no hay hot reload

A partir de aquí, importa prisma desde ese archivo en todos lados:

App Router (Next.js 13+) en Server Component:

// app/users/page.tsx
import { prisma } from '@/lib/prisma'

export default async function UsersPage() {
  const users = await prisma.user.findMany()
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

API Route:

// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET() {
  const users = await prisma.user.findMany()
  return NextResponse.json(users)
}

Pages Router (Next.js 12 y anteriores):

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { prisma } from '@/lib/prisma'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const users = await prisma.user.findMany()
  res.status(200).json(users)
}

Regla de oro: nunca hagas new PrismaClient() suelto en un archivo; importa siempre desde lib/prisma.ts.

En producción no te preocupes

¿Hay que hacer lo mismo en producción?

No. En Vercel u otro hosting, cada despliegue arranca limpio, no hay hot reload y el código es estático tras el build.

Por eso if (process.env.NODE_ENV !== 'production') deja singleton solo en desarrollo.

Es la recomendación oficial de Prisma. Si sigues este patrón, el problema desaparece.

La trampa pilla a muchos, pero entendida la causa es cuestión de un archivo. Configura lib/prisma.ts y olvídate de fugas de conexión.

Buenas prácticas de diseño de Schema

Entorno listo y fuga resuelta: toca diseñar tablas. Todo vive en prisma/schema.prisma.

Definición básica de modelos

Ejemplo simple de un blog con User y Post:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  password  String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Puntos clave:

  • @id: clave primaria
  • @default(autoincrement()): ID autoincremental
  • @unique: restricción única (por ejemplo, email)
  • String?: campo opcional (puede ser null)
  • @default(now()): fecha de creación automática
  • @updatedAt: actualiza la fecha en cada modificación

Convenciones de nombres:

  • Modelos en PascalCase: User, Post, UserProfile
  • Campos en camelCase: createdAt, authorId
  • Prisma convierte tablas a minúsculas y plural: Userusers, Postposts

Relaciones: uno a muchos, muchos a muchos, uno a uno

Las relaciones son el corazón del Schema y donde más se confunde al principio. Vamos una a una.

Uno a muchos (One-to-Many)

Un usuario puede tener varios artículos; un artículo pertenece a un solo usuario.

En el ejemplo anterior:

  • User tiene posts Post[]
  • Post tiene author User y authorId Int

@relation(fields: [authorId], references: [id]) define la clave foránea:

  • fields: [authorId] apunta al campo del modelo actual (Post)
  • references: [id] apunta al id de User

Muchos a muchos (Many-to-Many)

Un artículo puede tener varias etiquetas y una etiqueta puede estar en varios artículos.

Prisma admite relación implícita o tabla intermedia explícita.

Implícita (simple; Prisma gestiona la tabla puente):

model Post {
  id    Int    @id @default(autoincrement())
  title String
  tags  Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[]
}

Prisma crea _PostToTag automáticamente. En consultas usas include: { tags: true }.

Explícita (flexible; puedes añadir campos en la tabla intermedia):

model Post {
  id       Int        @id @default(autoincrement())
  title    String
  postTags PostTag[]
}

model Tag {
  id       Int        @id @default(autoincrement())
  name     String
  postTags PostTag[]
}

model PostTag {
  id        Int      @id @default(autoincrement())
  postId    Int
  tagId     Int
  post      Post     @relation(fields: [postId], references: [id])
  tag       Tag      @relation(fields: [tagId], references: [id])
  createdAt DateTime @default(now())
  
  @@unique([postId, tagId]) // evita duplicados
}

La explícita permite campos extra, como createdAt para saber cuándo se etiquetó. Si no necesitas nada más, la implícita basta.

Uno a uno (One-to-One)

Un usuario tiene un perfil; un perfil pertenece a un usuario.

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  profile Profile?
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String?
  userId Int    @unique
  user   User   @relation(fields: [userId], references: [id])
}

Clave: @unique en userId garantiza un solo Profile por User.

Técnicas avanzadas

Enum

Para campos con valores fijos:

enum Role {
  USER
  ADMIN
  MODERATOR
}

model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  role  Role   @default(USER)
}

Índice único compuesto

Varios campos juntos deben ser únicos:

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  authorId Int
  slug     String
  
  @@unique([authorId, slug]) // el slug no se repite para el mismo autor
}

Resolver ambigüedad en relaciones

Si dos modelos tienen varias relaciones entre sí, usa name:

model User {
  id             Int    @id @default(autoincrement())
  writtenPosts   Post[] @relation("PostAuthor")
  favoritePosts  Post[] @relation("PostFavorites")
}

model Post {
  id          Int    @id @default(autoincrement())
  title       String
  authorId    Int
  author      User   @relation("PostAuthor", fields: [authorId], references: [id])
  favoritedBy User[] @relation("PostFavorites")
}

Sin name, Prisma no sabe qué relación corresponde a qué.

Ejemplo completo: sistema de blog

Schema completo reuniendo lo anterior:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  USER
  ADMIN
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String?
  password  String
  role      Role      @default(USER)
  posts     Post[]
  comments  Comment[]
  profile   Profile?
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  avatar String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id])
}

model Post {
  id        Int       @id @default(autoincrement())
  title     String
  content   String?
  published Boolean   @default(false)
  authorId  Int
  author    User      @relation(fields: [authorId], references: [id])
  comments  Comment[]
  tags      Tag[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Comment {
  id        Int      @id @default(autoincrement())
  content   String
  postId    Int
  post      Post     @relation(fields: [postId], references: [id])
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

Con el Schema listo, ejecuta npx prisma migrate dev --name init para generar y aplicar la migración. Prisma crea tablas, índices y claves foráneas.

Diseñar Schema no es tan difícil: entiende uno a muchos, muchos a muchos y uno a uno, y añade enums e índices únicos cuando haga falta.

CRUD en la práctica

Schema listo: toca escribir código. La API de consultas de Prisma es muy directa.

Recuerda importar prisma desde lib/prisma.ts; no crees new PrismaClient() por tu cuenta.

Crear datos (Create)

Un solo registro:

import { prisma } from '@/lib/prisma'

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Alice',
    password: 'hashed_password_here'
  }
})

Creación en lote:

const users = await prisma.user.createMany({
  data: [
    { email: '[email protected]', name: 'Bob', password: 'pass1' },
    { email: '[email protected]', name: 'Charlie', password: 'pass2' }
  ],
  skipDuplicates: true // omite emails duplicados
})

console.log(`Se crearon ${users.count} usuarios`)

Creación anidada (usuario + Profile a la vez):

const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Dave',
    password: 'pass',
    profile: {
      create: {
        bio: 'Un desarrollador apasionado por la programación'
      }
    }
  },
  include: {
    profile: true // el resultado incluye profile
  }
})

Consultar datos (Read)

Un registro:

// por campo único
const user = await prisma.user.findUnique({
  where: { email: '[email protected]' }
})

// primer registro que coincida
const firstPost = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' }
})

Varios registros:

const users = await prisma.user.findMany({
  where: {
    email: {
      contains: '@gmail.com' // email contiene @gmail.com
    }
  },
  orderBy: { createdAt: 'desc' },
  take: 10, // solo 10
  skip: 0   // saltar 0 (paginación)
})

Consultas relacionales:

Con include:

const user = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true,      // todos los artículos del usuario
    profile: true     // perfil del usuario
  }
})

Con select (solo campos necesarios, mejor rendimiento):

const user = await prisma.user.findUnique({
  where: { id: 1 },
  select: {
    id: true,
    email: true,
    posts: {
      select: {
        id: true,
        title: true
      }
    }
  }
})
// solo id, email y posts (id + title)

include trae todos los campos; select solo los indicados. Con muchos datos, select ahorra ancho de banda.

Filtros:

Prisma admite filtros variados:

const posts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: 'Next.js' } },
      { content: { contains: 'Prisma' } }
    ],
    AND: [
      { published: true },
      { authorId: { not: 1 } } // excluye autor con id 1
    ]
  }
})

Operadores habituales:

  • equals: igual
  • not: distinto
  • in: dentro de un array (in: [1, 2, 3])
  • notIn: fuera del array
  • contains: contiene (cadena)
  • startsWith: empieza por
  • endsWith: termina en
  • gt/gte: mayor / mayor o igual
  • lt/lte: menor / menor o igual

Actualizar datos (Update)

Un registro:

const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Alice Updated' }
})

Varios registros:

const result = await prisma.user.updateMany({
  where: { email: { contains: '@gmail.com' } },
  data: { role: 'ADMIN' }
})

console.log(`Se actualizaron ${result.count} usuarios`)

Upsert (actualiza si existe, crea si no):

const user = await prisma.user.upsert({
  where: { email: '[email protected]' },
  update: { name: 'Alice Updated' },
  create: {
    email: '[email protected]',
    name: 'Alice',
    password: 'pass'
  }
})

Muy útil: evitas consultar primero para decidir entre create y update.

Eliminar datos (Delete)

Un registro:

const user = await prisma.user.delete({
  where: { id: 1 }
})

Varios registros:

const result = await prisma.user.deleteMany({
  where: {
    createdAt: {
      lt: new Date('2023-01-01') // elimina usuarios creados antes de 2023
    }
  }
})

console.log(`Se eliminaron ${result.count} usuarios`)

Transacciones

A veces necesitas que varias operaciones tengan éxito juntas o fallen juntas. Ejemplo: transferencia entre cuentas.

Transacción por lote (operaciones independientes):

const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: '[email protected]', password: 'pass' } }),
  prisma.post.create({ data: { title: 'Test Post', authorId: 1 } })
])

O ambas se confirman o ambas se revierten.

Transacción interactiva (con dependencias entre pasos):

const transferMoney = await prisma.$transaction(async (tx) => {
  // debitar cuenta A
  const accountA = await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 100 } }
  })
  
  if (accountA.balance < 0) {
    throw new Error('Saldo insuficiente')
  }
  
  // acreditar cuenta B
  const accountB = await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 100 } }
  })
  
  return { accountA, accountB }
})

Si algo falla a mitad, toda la transacción se revierte.

Ejemplo completo: API Route de Next.js

CRUD completo en rutas API:

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

// GET /api/posts - listado de artículos
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const page = parseInt(searchParams.get('page') || '1')
    const limit = parseInt(searchParams.get('limit') || '10')
    
    const posts = await prisma.post.findMany({
      where: { published: true },
      include: {
        author: {
          select: { id: true, name: true, email: true }
        },
        tags: true
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * limit,
      take: limit
    })
    
    const total = await prisma.post.count({ where: { published: true } })
    
    return NextResponse.json({ posts, total, page, limit })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  }
}

// POST /api/posts - crear artículo
export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { title, content, authorId, tagIds } = body
    
    const post = await prisma.post.create({
      data: {
        title,
        content,
        authorId,
        tags: {
          connect: tagIds.map((id: number) => ({ id })) // enlaza etiquetas existentes
        }
      },
      include: { tags: true }
    })
    
    return NextResponse.json(post, { status: 201 })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
  }
}
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

// GET /api/posts/:id - artículo individual
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const post = await prisma.post.findUnique({
      where: { id: parseInt(params.id) },
      include: {
        author: { select: { id: true, name: true } },
        tags: true,
        comments: {
          include: {
            author: { select: { id: true, name: true } }
          }
        }
      }
    })
    
    if (!post) {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 })
    }
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
  }
}

// PATCH /api/posts/:id - actualizar artículo
export async function PATCH(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json()
    const post = await prisma.post.update({
      where: { id: parseInt(params.id) },
      data: body
    })
    
    return NextResponse.json(post)
  } catch (error) {
    return NextResponse.json({ error: 'Failed to update post' }, { status: 500 })
  }
}

// DELETE /api/posts/:id - eliminar artículo
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    await prisma.post.delete({
      where: { id: parseInt(params.id) }
    })
    
    return NextResponse.json({ message: 'Post deleted' })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 })
  }
}

Con create, findUnique, findMany, update y delete, más filtros, relaciones y transacciones, cubres la mayoría de escenarios.

Técnicas avanzadas y problemas frecuentes

Ya dominas CRUD básico; en proyectos reales aparecen más matices. Aquí van optimización, migraciones y depuración.

Optimización de rendimiento

Usa select para reducir campos

Por defecto Prisma trae todos los campos. Si hay columnas grandes, select ayuda:

// malo: trae todo, incluido content potencialmente enorme
const posts = await prisma.post.findMany()

// mejor: solo lo necesario
const posts = await prisma.post.findMany({
  select: {
    id: true,
    title: true,
    createdAt: true,
    author: {
      select: { name: true }
    }
  }
})

En listados no necesitas el contenido completo: título y resumen bastan.

Evita N+1

Clásico en ORM: consultas N registros y luego una consulta por relación = N+1.

Con include, Prisma lo resuelve:

// malo: N+1
const users = await prisma.user.findMany()
for (const user of users) {
  user.posts = await prisma.post.findMany({ where: { authorId: user.id } })
}

// bien: una consulta
const users = await prisma.user.findMany({
  include: { posts: true }
})

Prisma combina consultas con JOIN o batch queries.

Configuración del pool

El pool por defecto es num_cpus * 2 + 1. En serverless (Vercel) o alta concurrencia puede hacer falta ajustarlo.

En DATABASE_URL:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=5"

En serverless conviene empezar bajo (por ejemplo 1) y subir poco a poco. Un pool demasiado grande agota conexiones de la base.

En Vercel, Prisma Accelerate ofrece pool HTTP y caché global optimizados para serverless.

Gestión de migraciones

Desarrollo

Tras cambiar el Schema:

npx prisma migrate dev --name add-user-role

Eso:

  1. Detecta cambios en el Schema
  2. Genera archivos SQL de migración
  3. Aplica la migración
  4. Regenera Prisma Client

Usa nombres descriptivos en --name, como add-user-role o create-post-table.

Producción

En producción nunca uses prisma migrate dev: hay riesgo de pérdida de datos. Usa prisma migrate deploy:

npx prisma migrate deploy

Solo aplica migraciones existentes; no genera nuevas.

En CI/CD, ejecuta prisma migrate deploy antes del despliegue para mantener schema y código alineados.

Revertir migraciones

Prisma no tiene rollback integrado. Opciones:

  1. Ver historial:
npx prisma migrate status
  1. Revertir manualmente con SQL o restaurar un snapshot de la base

En producción, haz backups regulares.

Depuración

Activar logs de consultas

Para ver el SQL ejecutado:

// lib/prisma.ts
export const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error']
})

Muy útil en desarrollo: SQL, tiempos y parámetros.

En producción, limita a errores:

log: ['error']

Prisma Studio

Herramienta visual:

npx prisma studio

En el navegador puedes:

  • Ver tablas y datos
  • Añadir, editar y eliminar registros
  • Probar relaciones

Sin cambiar a un cliente SQL externo.

Errores frecuentes

P2002: Unique constraint failed
→ Violación de unicidad (email duplicado, etc.). Comprueba si el registro ya existe.

P2025: Record not found
→ El registro a actualizar o eliminar no existe. Usa findUnique antes.

P1001: Can't reach database server
→ Fallo de conexión. Revisa DATABASE_URL y que la base esté levantada.

Notas para desplegar en Vercel

Añade prisma generate al script build en package.json:

{
  "scripts": {
    "build": "prisma generate && next build"
  }
}

Así cada build genera el cliente actualizado y los tipos coinciden con el Schema.

Configura DATABASE_URL en el panel de Vercel; no la hardcodees.

Con PostgreSQL, Vercel Postgres o Supabase suelen ser opciones sencillas con plan gratuito.

Resumen de la sección avanzada

  • Rendimiento: select para menos campos, include contra N+1, pool bien dimensionado
  • Migraciones: migrate dev en desarrollo, migrate deploy en producción
  • Depuración: logs, Prisma Studio, códigos de error habituales
  • Despliegue: prisma generate en build y variables de entorno correctas

Con esto, Prisma se siente mucho más fluido.

Conclusión

Desde montar el entorno hasta CRUD, pasando por fugas de conexión y diseño de Schema, has recorrido el flujo completo de Next.js + Prisma.

Los puntos más importantes, otra vez:

Fuga de conexiones: crea lib/prisma.ts con singleton e importa desde ahí. Obligatorio en desarrollo; si no, el pool acabará explotando.

Diseño de Schema: entiende uno a muchos, muchos a muchos y uno a uno, y cómo usar @relation. Nombres consistentes y enums cuando toque.

CRUD: domina create, findMany, update, delete, la diferencia entre include y select, y $transaction para consistencia.

Rendimiento y despliegue: select para menos datos, include contra N+1, migraciones según entorno, y prisma generate al desplegar en Vercel.

La curva de Prisma es más suave que la del SQL puro; el tipado y la experiencia de desarrollo compensan. No es perfecto: hay overhead y consultas muy complejas pueden requerir SQL nativo. Pero en la mayoría de proyectos full stack con Next.js, las ventajas son claras.

Próximos pasos:

  • Monta un proyecto simple Next.js + Prisma y practica
  • Lee la documentación oficial de Prisma para funciones avanzadas
  • Prueba Prisma Studio para gestionar datos visualmente
  • Revisa GitHub Discussions de Prisma: la comunidad es activa y casi cualquier duda tiene respuesta

Si tienes dudas, comenta abajo o comparte las trampas que te hayan pillado con Prisma. Tu experiencia puede ayudar a otros.

Flujo completo de configuración de Next.js + Prisma

Pasos completos desde la instalación hasta resolver fugas de conexión, diseño de Schema y operaciones CRUD

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Instalar e inicializar Prisma

    Instalar dependencias:
    • npm install prisma @prisma/client
    • npx prisma init

    La inicialización crea:
    • prisma/schema.prisma: archivo de definición del Schema
    • .env: variables de entorno (incluye DATABASE_URL)

    Configurar la conexión:
    • Definir DATABASE_URL en .env
    • Formato: postgresql://user:password@localhost:5432/dbname
  2. 2

    Step 2: Resolver el problema de fuga de conexiones

    Crear patrón singleton:
    • Crear el archivo lib/prisma.ts
    • En desarrollo, cachear la instancia con globalThis
    • En producción, exportar la instancia directamente

    Código:
    const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
    export const prisma = globalForPrisma.prisma || new PrismaClient()
    if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

    Así evitas que el hot reload cree múltiples conexiones
  3. 3

    Step 3: Diseñar el Schema

    Definir modelos:
    • Usar la palabra clave model para las tablas
    • Marcar la clave primaria con @id
    • Definir valores por defecto con @default
    • Definir relaciones con @relation

    Tipos de relación:
    • Uno a uno: @relation(fields, references)
    • Uno a muchos: un modelo lleva @relation y el otro no
    • Muchos a muchos: tabla intermedia (@relation en la tabla puente)

    Generar migraciones:
    • npx prisma migrate dev --name init
  4. 4

    Step 4: Implementar operaciones CRUD

    Crear datos:
    • prisma.user.create({ data: { name, email } })

    Consultar datos:
    • prisma.user.findMany(): consultar varios registros
    • prisma.user.findUnique({ where: { id } }): consultar uno
    • prisma.user.findFirst({ where: { ... } }): consultar el primero

    Actualizar datos:
    • prisma.user.update({ where: { id }, data: { name } })

    Eliminar datos:
    • prisma.user.delete({ where: { id } })
  5. 5

    Step 5: Gestionar consultas relacionales

    Usar include:
    • prisma.user.findMany({ include: { posts: true } })
    • Devuelve usuarios con todos sus artículos

    Usar select:
    • prisma.user.findMany({ select: { id: true, name: true } })
    • Devuelve solo los campos indicados y reduce datos consultados

    Evitar el problema N+1:
    • Usa include para traer datos relacionados de una vez
    • No consultes relaciones dentro de un bucle externo
  6. 6

    Step 6: Despliegue y migraciones

    Despliegue en producción:
    • Configurar la variable DATABASE_URL
    • Ejecutar npx prisma generate para generar el cliente
    • Ejecutar npx prisma migrate deploy para aplicar migraciones

    Despliegue en Vercel:
    • Configurar variables de entorno en Vercel Dashboard
    • Añadir prisma generate al comando build
    • Usar prisma migrate deploy para aplicar migraciones

    Importante: no ejecutes migrate dev en producción

FAQ

¿Por qué aparece el error 'too many clients already'?
Es un problema de fuga de conexiones causado por el hot reload de Next.js.

Cada vez que cambias el código, Next.js crea una nueva instancia de PrismaClient, pero las conexiones antiguas no se cierran solas y acaban agotando el pool.

La solución es usar un patrón singleton y cachear la instancia de PrismaClient con globalThis en desarrollo.
¿Qué diferencias hay entre Prisma, TypeORM y Drizzle?
Prisma:
• Mejor seguridad de tipos y mejor experiencia de desarrollo
• Pero con cierto coste de rendimiento

TypeORM:
• Potente y soporta consultas complejas
• Pero la configuración es más compleja

Drizzle:
• Ligero y con buen rendimiento
• Pero la seguridad de tipos no iguala a Prisma

En proyectos Next.js, la facilidad de uso y el tipado de Prisma suelen pesar más.
¿Cómo diseñar una relación uno a muchos?
Añade el campo de clave foránea en el lado 'muchos' y márcalo con @relation.

Ejemplo: un User tiene varios Post
• En el modelo Post, añade userId y @relation(fields: ['userId'], references: [id])
• En el modelo User, añade posts Post[]
¿Qué diferencia hay entre include y select?
include:
• Sirve para consultas relacionales y devuelve datos asociados
• Aumenta la cantidad de datos consultados

select:
• Sirve para elegir campos y devolver solo los indicados
• Reduce la cantidad de datos consultados

Puedes combinarlos: { include: { posts: true }, select: { id: true, name: true } }
¿Cómo evitar el problema de consultas N+1?
Usa include para traer datos relacionados de una vez, en lugar de consultarlos en un bucle externo. Por ejemplo: prisma.user.findMany({ include: { posts: true } }) obtiene todos los usuarios y sus artículos en una sola operación, en vez de consultar los artículos usuario por usuario.
¿Prisma soporta transacciones?
Sí. Usa prisma.$transaction([...]) para ejecutar varias operaciones: o todas tienen éxito o todas fallan. Ejemplo: await prisma.$transaction([prisma.user.create(...), prisma.post.create(...)]).
¿Cómo desplegar Prisma en Vercel?
Pasos:
1) Configura DATABASE_URL en Vercel Dashboard
2) Añade prisma generate al comando build en package.json
3) Usa prisma migrate deploy para aplicar migraciones (no uses migrate dev)

Asegúrate de que la conexión a la base de datos en producción funcione correctamente.

20 min de lectura · Publicado el: 20 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog