Que es Prisma y por que usarlo
Prisma es un ORM (Object-Relational Mapping) de nueva generacion para Node.js y TypeScript que reemplaza a ORMs tradicionales como Sequelize o TypeORM con una experiencia de desarrollo radicalmente superior. En lugar de escribir SQL crudo o depender de query builders en tiempo de ejecucion, Prisma genera un cliente completamente tipado a partir de tu esquema de base de datos, proporcionandote autocompletado, verificacion de errores en tiempo de compilacion y patrones de acceso a datos intuitivos.
Cuando se combina con el App Router de Next.js, Prisma permite un flujo de trabajo full-stack muy potente: defines tu esquema de base de datos en un solo archivo, generas consultas type-safe y las usas directamente en Server Components, Route Handlers y Server Actions. No necesitas una capa API separada ni definiciones de tipos manuales: Prisma se encarga de todo.
Ventajas clave de Prisma frente a ORMs tradicionales:
- Type safety: Cada consulta esta completamente tipada segun tu esquema. Si renombras una columna, TypeScript detecta cada referencia rota en tiempo de compilacion.
- Cliente autogenerado: El Prisma Client se genera a partir de tu esquema, lo que significa que tu capa de acceso a datos siempre esta sincronizada con la estructura de tu base de datos.
- Migraciones declarativas: Los cambios en el esquema se expresan de forma declarativa, y Prisma genera los archivos de migracion SQL automaticamente.
- Agnostico de base de datos: Prisma soporta PostgreSQL, MySQL, SQLite, SQL Server, MongoDB y CockroachDB con la misma API.
- Prisma Studio: Un explorador visual de base de datos para inspeccionar y editar datos durante el desarrollo.
Instalacion y configuracion inicial
Empieza instalando Prisma como dependencia de desarrollo y el Prisma Client como dependencia de produccion en tu proyecto Next.js:
# Instalar CLI de Prisma y Client
npm install prisma --save-dev
npm install @prisma/client
# Inicializar Prisma con PostgreSQL como datasource
npx prisma init --datasource-provider postgresql
Este comando crea dos archivos: un archivo prisma/schema.prisma donde defines
tus modelos de datos, y un archivo .env con una URL de conexion placeholder.
Actualiza el archivo .env con la URL de conexion real de tu base de datos:
# .env
DATABASE_URL="postgresql://usuario:password@localhost:5432/midb?schema=public"
El archivo schema.prisma es el corazon de tu configuracion de Prisma. Contiene
tres secciones: la configuracion del datasource, la configuracion del generador y tus modelos
de datos:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Los modelos van aquiImportante: Nunca hagas commit de tu archivo
.enval control de versiones. Anadelo a tu.gitignorey usa variables de entorno en tu plataforma de despliegue.
Definicion de modelos con relaciones
Los modelos de Prisma se mapean directamente a tablas de la base de datos. Cada campo en un modelo corresponde a una columna, y puedes definir relaciones entre modelos usando una sintaxis especial. Aqui tienes un ejemplo practico con modelos User, Post y Category:
// 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?
avatar String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model Profile {
id String @id @default(cuid())
bio String?
website String?
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profiles")
}
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
excerpt String?
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
categories Category[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([slug])
@@map("posts")
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
posts Post[]
@@map("categories")
}
enum Role {
USER
ADMIN
EDITOR
}Conceptos clave de este esquema:
@idmarca la clave primaria.@default(cuid())genera un ID unico resistente a colisiones.@uniquecrea una restriccion de unicidad en la columna.@relationdefine relaciones de clave foranea. El parametrofieldsespecifica la columna local yreferencesla columna destino.@@indexcrea indices de base de datos para columnas consultadas frecuentemente.@@mappersonaliza el nombre de la tabla en la base de datos (Prisma usa el nombre del modelo por defecto).- El campo
Post[]en User es un campo de relacion virtual: no crea una columna pero te permite consultar los posts relacionados. - La relacion many-to-many implicita entre Post y Category es gestionada automaticamente por Prisma a traves de una tabla de union.
Migraciones: prisma migrate dev y deploy
Prisma Migrate traduce los cambios de tu esquema en archivos de migracion SQL. Durante el
desarrollo, usa prisma migrate dev para crear y aplicar migraciones:
# Crear y aplicar una nueva migracion
npx prisma migrate dev --name init
# Este comando hace tres cosas:
# 1. Genera un archivo de migracion SQL en prisma/migrations/
# 2. Aplica la migracion a tu base de datos
# 3. Regenera el Prisma Client
Cada migracion crea una carpeta con marca de tiempo que contiene un archivo migration.sql
con las sentencias SQL crudas. Estos archivos deben ser commiteados al control de versiones
para que tu equipo pueda reproducir el esquema de la base de datos.
Cuando modificas tu esquema (por ejemplo, anadiendo un nuevo campo), ejecuta migrate dev de nuevo:
// Anadir nuevos campos al modelo Post
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
excerpt String?
coverImage String? // Nuevo campo
readTime Int? // Nuevo campo
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
categories Category[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([slug])
@@map("posts")
}# Generar migracion para los nuevos campos
npx prisma migrate dev --name add-post-cover-and-readtime
Para despliegues en produccion, usa prisma migrate deploy. Este comando aplica
las migraciones pendientes sin generar nuevas:
# Aplicar migraciones pendientes en produccion
npx prisma migrate deployOtros comandos utiles del CLI de Prisma:
# Resetear la base de datos (elimina todos los datos y re-aplica migraciones)
npx prisma migrate reset
# Generar Prisma Client sin ejecutar migraciones
npx prisma generate
# Push de cambios del esquema directamente sin crear archivo de migracion (solo prototipado)
npx prisma db push
# Seed de la base de datos con datos iniciales
npx prisma db seedPatron Singleton del Prisma Client para Next.js
En desarrollo, Next.js usa hot module reloading (HMR) que causa que los modulos se re-evaluen frecuentemente. Si instancias un nuevo Prisma Client en cada recarga del modulo, agotas rapidamente las conexiones de tu base de datos. La solucion es un patron singleton que reutiliza la misma instancia del cliente entre recargas.
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
Este patron almacena el Prisma Client en el objeto globalThis, que persiste
entre recargas de HMR. En produccion, se crea un nuevo cliente normalmente ya que HMR no aplica.
La configuracion de log habilita el logging de consultas durante el desarrollo para depuracion.
Importa el singleton donde necesites acceso a la base de datos:
import { prisma } from "@/lib/prisma";Consejo: Si usas Prisma con herramientas de connection pooling como PgBouncer, anade
?pgbouncer=truea tu URL de conexion y configura ladirectUrlpara migraciones. Consulta la seccion de connection pooling mas abajo para mas detalles.
Operaciones CRUD con Server Components y Route Handlers
Con el singleton de Prisma configurado, puedes consultar la base de datos directamente en Server Components, Server Actions y Route Handlers. No necesitas una capa API separada.
Leer datos en Server Components
// app/[locale]/blog/page.tsx
import { prisma } from "@/lib/prisma";
export default async function BlogPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true, avatar: true },
},
categories: true,
},
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>Por {post.author.name}</p>
<p>{post.excerpt}</p>
<div>
{post.categories.map((cat) => (
<span key={cat.id}>{cat.name}</span>
))}
</div>
</li>
))}
</ul>
</main>
);
}Crear datos con Server Actions
// app/[locale]/blog/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
excerpt: z.string().max(300).optional(),
slug: z.string().regex(/^[a-z0-9-]+$/),
categoryIds: z.array(z.string()).optional(),
});
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get("title"),
content: formData.get("content"),
excerpt: formData.get("excerpt"),
slug: formData.get("slug"),
};
const validated = CreatePostSchema.parse(rawData);
const post = await prisma.post.create({
data: {
title: validated.title,
slug: validated.slug,
content: validated.content,
excerpt: validated.excerpt,
authorId: "current-user-id", // Reemplazar con autenticacion real
categories: validated.categoryIds
? { connect: validated.categoryIds.map((id) => ({ id })) }
: undefined,
},
});
revalidatePath("/blog");
return post;
}CRUD con Route Handlers
// app/api/posts/route.ts
import { prisma } from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") ?? "1");
const limit = parseInt(searchParams.get("limit") ?? "10");
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
where: { published: true },
skip,
take: limit,
orderBy: { createdAt: "desc" },
include: {
author: { select: { name: true, avatar: true } },
},
}),
prisma.post.count({ where: { published: true } }),
]);
return NextResponse.json({
data: posts,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
});
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = CreatePostSchema.parse(body);
const post = await prisma.post.create({
data: {
...validated,
authorId: "current-user-id",
},
});
return NextResponse.json(post, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Validacion fallida", details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Error interno del servidor" },
{ status: 500 }
);
}
}Actualizar y eliminar registros
// app/api/posts/[id]/route.ts
import { prisma } from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const body = await request.json();
const post = await prisma.post.update({
where: { id },
data: {
title: body.title,
content: body.content,
published: body.published,
categories: body.categoryIds
? { set: body.categoryIds.map((catId: string) => ({ id: catId })) }
: undefined,
},
});
return NextResponse.json(post);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await prisma.post.delete({ where: { id } });
return NextResponse.json({ success: true });
}Optimizacion de consultas
Prisma proporciona varias herramientas para optimizar tus consultas de base de datos. Entender estas opciones es critico para construir aplicaciones de alto rendimiento.
Select vs Include
Por defecto, Prisma devuelve todos los campos escalares de un modelo. Usa select
para obtener solo los campos que necesitas, o include para cargar adicionalmente
los modelos relacionados:
// select: Solo devuelve los campos especificados (reduce la transferencia de datos)
const postTitles = await prisma.post.findMany({
select: {
id: true,
title: true,
slug: true,
createdAt: true,
author: {
select: { name: true },
},
},
});
// include: Devuelve todos los campos escalares MAS los modelos relacionados
const postsWithAuthor = await prisma.post.findMany({
include: {
author: true,
categories: true,
},
});
// select anidado dentro de include
const detailed = await prisma.post.findUnique({
where: { slug: "mi-post" },
include: {
author: {
select: {
name: true,
email: true,
profile: {
select: { bio: true },
},
},
},
categories: {
select: { name: true, slug: true },
},
},
});Consejo de rendimiento: Siempre prefiere
selectsobreincludecuando solo necesitas campos especificos. Esto reduce la cantidad de datos transferidos desde la base de datos y acelera la serializacion.
Paginacion basada en cursor y por offset
// Paginacion por offset (simple pero mas lenta para grandes datasets)
const page = 2;
const pageSize = 20;
const posts = await prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: "desc" },
});
// Paginacion basada en cursor (mas rapida para grandes datasets)
const posts = await prisma.post.findMany({
take: 20,
skip: 1, // Saltar el cursor en si
cursor: {
id: lastPostId, // El ID del ultimo elemento de la pagina anterior
},
orderBy: { createdAt: "desc" },
});Consultas raw para operaciones complejas
// Usa consultas raw cuando la API de consultas de Prisma no sea suficiente
const popularPosts = await prisma.$queryRaw`
SELECT p.id, p.title, p.slug, COUNT(v.id) as view_count
FROM posts p
LEFT JOIN post_views v ON v.post_id = p.id
WHERE p.published = true
GROUP BY p.id
ORDER BY view_count DESC
LIMIT 10
`;
// Consultas parametrizadas para prevenir inyeccion SQL
const searchTerm = "prisma";
const results = await prisma.$queryRaw`
SELECT * FROM posts
WHERE title ILIKE ${'%' + searchTerm + '%'}
AND published = true
ORDER BY created_at DESC
`;Trabajar con relaciones
Relaciones uno-a-muchos
// Crear un usuario con posts en una sola transaccion
const userWithPosts = await prisma.user.create({
data: {
email: "maria@ejemplo.com",
name: "Maria Garcia",
posts: {
create: [
{
title: "Mi primer post",
slug: "mi-primer-post",
content: "Hola mundo!",
},
{
title: "Mi segundo post",
slug: "mi-segundo-post",
content: "Otro post mas.",
},
],
},
},
include: { posts: true },
});
// Consultar posts de un usuario especifico
const userPosts = await prisma.post.findMany({
where: { authorId: "user-id" },
orderBy: { createdAt: "desc" },
});Relaciones muchos-a-muchos
// Conectar categorias existentes a un post
const post = await prisma.post.update({
where: { id: "post-id" },
data: {
categories: {
connect: [
{ id: "category-1" },
{ id: "category-2" },
],
},
},
include: { categories: true },
});
// Desconectar una categoria de un post
const updated = await prisma.post.update({
where: { id: "post-id" },
data: {
categories: {
disconnect: [{ id: "category-1" }],
},
},
});
// Reemplazar todas las categorias de un post (set reemplaza toda la relacion)
const replaced = await prisma.post.update({
where: { id: "post-id" },
data: {
categories: {
set: [{ id: "category-3" }, { id: "category-4" }],
},
},
});
// Consultar posts por categoria
const techPosts = await prisma.post.findMany({
where: {
categories: {
some: { slug: "tecnologia" },
},
},
});Transacciones
// Transaccion interactiva para operaciones complejas
const result = await prisma.$transaction(async (tx) => {
// Crear una nueva categoria
const category = await tx.category.create({
data: { name: "Nueva Categoria", slug: "nueva-categoria" },
});
// Crear un post y conectarlo a la categoria
const post = await tx.post.create({
data: {
title: "Ejemplo de transaccion",
slug: "ejemplo-de-transaccion",
content: "Este post fue creado en una transaccion.",
authorId: "user-id",
categories: { connect: { id: category.id } },
},
});
// Actualizar estadisticas del usuario
await tx.user.update({
where: { id: "user-id" },
data: { /* actualizar stats */ },
});
return { category, post };
});
// Si alguna operacion falla, todos los cambios se reviertenPrisma Studio para depuracion
Prisma Studio es un explorador visual de base de datos que se abre en tu navegador. Te permite ver, crear, actualizar y eliminar registros sin escribir codigo, algo invaluable durante el desarrollo y la depuracion.
# Lanzar Prisma Studio
npx prisma studio
# Se abre en http://localhost:5555 por defectoFuncionalidades de Prisma Studio:
- Explorar todas las tablas y sus registros en una interfaz tipo hoja de calculo
- Filtrar y ordenar registros por cualquier columna
- Editar registros inline y guardar cambios directamente en la base de datos
- Navegar relaciones haciendo clic en registros relacionados
- Crear nuevos registros con una interfaz basada en formularios
- Eliminar registros con dialogos de confirmacion
Aunque Prisma Studio es excelente para desarrollo, nunca lo uses en entornos de produccion. Para la administracion de bases de datos en produccion, usa herramientas de administracion adecuadas con controles de acceso y registro de auditoria.
Buenas practicas
Manejo de errores
import { Prisma } from "@prisma/client";
async function createUser(email: string, name: string) {
try {
const user = await prisma.user.create({
data: { email, name },
});
return { success: true, data: user };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// P2002: Violacion de restriccion de unicidad
if (error.code === "P2002") {
return {
success: false,
error: "Ya existe un usuario con este email.",
};
}
// P2025: Registro no encontrado
if (error.code === "P2025") {
return {
success: false,
error: "Registro no encontrado.",
};
}
}
// Re-lanzar errores inesperados
throw error;
}
}Connection Pooling con PgBouncer
En entornos serverless como Vercel, cada invocacion de funcion puede crear una nueva conexion a la base de datos. Sin connection pooling, esto agota rapidamente el limite de conexiones de tu base de datos. Usa PgBouncer o un pooler gestionado como el connection pooler integrado de Supabase:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // Conexion pooled para consultas
directUrl = env("DIRECT_URL") // Conexion directa para migraciones
}# .env
# Conexion pooled (via PgBouncer) para consultas de la aplicacion
DATABASE_URL="postgresql://user:pass@pooler.ejemplo.com:6543/mydb?pgbouncer=true"
# Conexion directa para migraciones (sin pasar por el pooler)
DIRECT_URL="postgresql://user:pass@db.ejemplo.com:5432/mydb"
La directUrl es utilizada por prisma migrate porque las migraciones
requieren una conexion directa que soporte sentencias DDL y advisory locks, que no son
compatibles con el modo de pooling de transacciones de PgBouncer.
Seeding de la base de datos
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// Limpiar datos existentes
await prisma.post.deleteMany();
await prisma.category.deleteMany();
await prisma.user.deleteMany();
// Crear usuarios
const admin = await prisma.user.create({
data: {
email: "admin@ejemplo.com",
name: "Usuario Admin",
role: "ADMIN",
profile: {
create: {
bio: "Administrador de la plataforma",
website: "https://ejemplo.com",
},
},
},
});
// Crear categorias
const categories = await Promise.all(
["Tecnologia", "Diseno", "Negocios"].map((name) =>
prisma.category.create({
data: { name, slug: name.toLowerCase() },
})
)
);
// Crear posts
await prisma.post.create({
data: {
title: "Empezando con Prisma",
slug: "empezando-con-prisma",
content: "Una guia completa de Prisma ORM...",
excerpt: "Aprende a usar Prisma con Next.js",
published: true,
authorId: admin.id,
categories: {
connect: [{ id: categories[0].id }],
},
},
});
console.log("Base de datos seeded correctamente");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});// package.json
{
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
}# Ejecutar el script de seed
npx prisma db seedExtensiones del Prisma Client
Las extensiones del Prisma Client te permiten anadir comportamiento personalizado a tus consultas. Aqui tienes un ejemplo que anade funcionalidad de logging y deteccion de consultas lentas:
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const basePrisma = new PrismaClient();
export const prisma = basePrisma.$extends({
query: {
$allModels: {
async findMany({ model, operation, args, query }) {
const start = performance.now();
const result = await query(args);
const duration = performance.now() - start;
if (duration > 1000) {
console.warn(
`Consulta lenta detectada: ${model}.${operation} tardo ${duration.toFixed(0)}ms`
);
}
return result;
},
},
},
});Combinando estas buenas practicas (patron singleton, manejo correcto de errores, connection pooling, seeding y extensiones) construyes una capa de base de datos robusta y mantenible que escala con tu aplicacion Next.js.