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

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
selectreduces 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:
- Crea
prisma/schema.prisma(configuración central) - Crea
.envcon la variableDATABASE_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:
globalForPrismatipaglobalThispara TypeScriptexport const prisma = globalForPrisma.prisma || new PrismaClient()es el núcleo: reutiliza si ya existe; si no, crea una nuevaif (process.env.NODE_ENV !== 'production')guarda la instancia englobalThissolo 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:
User→users,Post→posts
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:
Usertieneposts Post[]Posttieneauthor UseryauthorId Int
@relation(fields: [authorId], references: [id]) define la clave foránea:
fields: [authorId]apunta al campo del modelo actual (Post)references: [id]apunta alidde 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: igualnot: distintoin: dentro de un array (in: [1, 2, 3])notIn: fuera del arraycontains: contiene (cadena)startsWith: empieza porendsWith: termina engt/gte: mayor / mayor o iguallt/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:
- Detecta cambios en el Schema
- Genera archivos SQL de migración
- Aplica la migración
- 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:
- Ver historial:
npx prisma migrate status
- 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:
selectpara menos campos,includecontra N+1, pool bien dimensionado - Migraciones:
migrate deven desarrollo,migrate deployen producción - Depuración: logs, Prisma Studio, códigos de error habituales
- Despliegue:
prisma generateen 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
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
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
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
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
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
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'?
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?
• 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?
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?
• 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?
¿Prisma soporta transacciones?
¿Cómo desplegar Prisma en Vercel?
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
Guía completa de Next.js
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Guía de bases de datos para Next.js: comparativa completa de PostgreSQL, MySQL, MongoDB y servicios en la nube
¿No sabes qué base de datos elegir para tu proyecto Next.js? Esta guía compara PostgreSQL, MySQL y MongoDB, analiza Vercel Postgres, Supabase, PlanetScale y MongoDB Atlas, y te ayuda a decidir rápido con 3 preguntas para evitar errores costosos.
Parte 22 de 51
Siguiente
Guía de gestión de estado en Next.js: Zustand vs Jotai en la práctica
¿Redux demasiado pesado y Context con mal rendimiento? Este artículo compara Zustand y Jotai en Next.js, ofrece una guía clara de elección y buenas prácticas con App Router para elegir la solución ligera adecuada.
Parte 24 de 51



Comentarios
Inicia sesión con GitHub para dejar un comentario