ORM de Prisma - Acceso a base de datos con seguridad de tipos con cliente generado automáticamente y migraciones
Receta
npm install prisma @prisma/client
npx prisma init// 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?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}npx prisma migrate dev --name init
npx prisma generate// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;Cuándo usarlo: Necesitas consultas de base de datos con seguridad de tipos en una aplicación Next.js con tipos generados automáticamente, manejo de relaciones y migraciones de esquema.
Ejemplo funcional
// app/posts/page.tsx (Server Component)
import { prisma } from "@/lib/prisma";
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: { select: { name: true, email: true } } },
orderBy: { createdAt: "desc" },
take: 20,
});
return (
<div className="max-w-3xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Publicaciones publicadas</h1>
{posts.map((post) => (
<article key={post.id} className="border-b py-4">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 text-sm">
por {post.author.name ?? "Anónimo"}
</p>
{post.content && <p className="mt-2">{post.content}</p>}
</article>
))}
</div>
);
}// app/posts/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const authorId = Number(formData.get("authorId"));
await prisma.post.create({
data: { title, content, authorId, published: false },
});
revalidatePath("/posts");
}
export async function publishPost(postId: number) {
await prisma.post.update({
where: { id: postId },
data: { published: true },
});
revalidatePath("/posts");
}
export async function deletePost(postId: number) {
await prisma.post.delete({ where: { id: postId } });
revalidatePath("/posts");
}// app/posts/new/page.tsx
"use client";
import { createPost } from "../actions";
export default function NewPostPage() {
return (
<form action={createPost} className="max-w-md mx-auto p-6 space-y-4">
<input type="hidden" name="authorId" value="1" />
<div>
<label className="block text-sm font-medium">Título</label>
<input
name="title"
required
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium">Contenido</label>
<textarea
name="content"
rows={5}
className="w-full border rounded px-3 py-2"
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Crear publicación
</button>
</form>
);
}Lo que esto demuestra:
- Consultas de Prisma directas en Server Components (no se necesita capa API)
- Server Actions para mutaciones con
revalidatePath - Incluyes de relaciones con selección de campos
- Patrón singleton para prevenir agotamiento de conexiones en desarrollo
- Consultas con seguridad de tipos con campos autocompletados
Análisis profundo
Cómo funciona
- Prisma genera un cliente de TypeScript a partir de tu archivo
schema.prisma, proporcionando campos de modelo autocompletados, recorrido de relaciones y filtros de consulta - El cliente generado vive en
node_modules/.prisma/clienty se regenera enprisma generateoprisma migrate dev - Las consultas devuelven objetos JavaScript simples (no instancias de clase), haciéndolos serializables y compatibles con Server Components
- Las relaciones usan claves foráneas definidas en el esquema;
includeyselectcontrolan qué datos relacionados se obtienen - Las migraciones son archivos SQL almacenados en
prisma/migrations/, rastreados por una tabla_prisma_migrationsen tu base de datos - El patrón singleton (
globalForPrisma) previene que hot reload de Next.js cree nuevas instancias dePrismaClient, lo que agoraría las conexiones de base de datos
Variaciones
Filtrado y paginación:
const results = await prisma.post.findMany({
where: {
AND: [
{ published: true },
{ title: { contains: searchQuery, mode: "insensitive" } },
],
},
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: "desc" },
});
const total = await prisma.post.count({
where: { published: true },
});Transacciones:
const [post, user] = await prisma.$transaction([
prisma.post.create({ data: { title: "Hello", authorId: 1 } }),
prisma.user.update({
where: { id: 1 },
data: { name: "Updated Name" },
}),
]);
// Interactive transaction
await prisma.$transaction(async (tx) => {
const user = await tx.user.findUnique({ where: { id: 1 } });
if (!user) throw new Error("User not found");
await tx.post.create({ data: { title: "Hello", authorId: user.id } });
});Upsert (crear o actualizar):
const user = await prisma.user.upsert({
where: { email: "alice@example.com" },
update: { name: "Alice Updated" },
create: { email: "alice@example.com", name: "Alice" },
});SQL sin procesar para consultas complejas:
const results = await prisma.$queryRaw<
{ id: number; title: string }[]
>`SELECT id, title FROM "Post" WHERE "published" = true LIMIT ${limit}`;Notas de TypeScript
- Prisma genera tipos para cada modelo:
User,Post, etc. - Los tipos de resultados de consultas se infieren basándose en
includeyselect— no se necesitan definiciones de tipos manuales - Usa
Prisma.PostCreateInputpara tipos de datos de creación,Prisma.PostWhereInputpara filtros Prisma.PostGetPayload<{ include: { author: true } }>te da el tipo de retorno exacto para una consulta con incluyes
import { Prisma } from "@prisma/client";
type PostWithAuthor = Prisma.PostGetPayload<{
include: { author: true };
}>;
function renderPost(post: PostWithAuthor) {
return `${post.title} by ${post.author.name}`;
}Problemas comunes
-
Agotamiento de conexiones en desarrollo — hot reload de Next.js crea nuevas instancias de PrismaClient. Solución: Usa el patrón singleton mostrado en la sección Receta. Almacena el cliente en
globalThis. -
Cliente generado obsoleto — Después de cambiar
schema.prisma, las consultas pueden no reflejar campos nuevos. Solución: Ejecutanpx prisma generatedespués de cambios de esquema.prisma migrate devlo hace automáticamente. -
Consultas N+1 — Acceder a relaciones en un bucle sin
includedispara una consulta por iteración. Solución: Usaincludeoselectpara cargar relaciones con entusiasmo en la consulta inicial. -
Serialización de BigInt — Los campos
BigIntno se pueden serializar a JSON para componentes cliente. Solución: Convierte BigInt a Number o String antes de pasar a componentes cliente:Number(post.viewCount). -
Los cambios de enum requieren migración — Agregar valores a una enumeración de Prisma requiere una migración, no solo
prisma generate. Solución: Ejecutanpx prisma migrate dev --name add-enum-value. -
Confusión de zona horaria de DateTime — Prisma almacena DateTime como UTC. Solución: Siempre maneja la conversión de zona horaria en el lado del cliente, no en consultas.
Alternativas
| Biblioteca | Mejor para | Compensación |
|---|---|---|
| Prisma | ORM con seguridad de tipos con migraciones | Runtime más pesado, cliente generado |
| Drizzle ORM | Ligero, sintaxis similar a SQL | Menos abstracción, migraciones manuales |
| Kysely | Generador de consultas con seguridad de tipos | Sin gestión de esquema o migraciones |
| TypeORM | ORM basado en decoradores | Menos seguridad de tipos, más pesado |
| Knex.js | Generador de consultas SQL sin procesar | Sin generación de tipos, tipos manuales |
Preguntas frecuentes
¿Por qué necesito el patrón singleton para PrismaClient en desarrollo de Next.js?
- Next.js hot module replacement crea un nuevo alcance de módulo en cada cambio de código
- Cada nuevo alcance instanciaría un nuevo
PrismaClient, abriendo conexiones de base de datos nuevas - Las bases de datos tienen un límite de conexión (p. ej., PostgreSQL por defecto es 100)
- Almacenar el cliente en
globalThisasegura que solo una instancia persista a través de hot reloads - En producción esto no es un problema porque el servidor se inicia una sola vez
¿Cuál es la diferencia entre include y select en consultas de Prisma?
includeobtiene todos los campos escalares del modelo padre más las relaciones especificadasselectobtiene solo los campos que explícitamente enumeraste, incluidas relaciones- No puedes usar tanto
includecomoselectal mismo nivel superior - Usa
selectcuando quieras minimizar los datos devueltos
¿Cómo ejecuto consultas de Prisma dentro de un Server Component?
import { prisma } from "@/lib/prisma";
export default async function Page() {
const users = await prisma.user.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}No se necesita capa API -- Server Components se ejecutan en el servidor y pueden consultar la base de datos directamente.
¿Cuándo debería usar prisma.$transaction y cuáles son las dos formas?
- Usa transacciones cuando múltiples operaciones deben tener éxito o fallar juntas
- Forma de matriz secuencial:
prisma.$transaction([query1, query2])ejecuta consultas en orden - Forma interactiva:
prisma.$transaction(async (tx) => { ... })te permite usar resultados intermedios - Las transacciones interactivas mantienen una conexión de base de datos por la duración, así que mantenlas breves
Problema: ¿Por qué mis consultas no reflejan campos de esquema nuevos después de que edito schema.prisma?
- El Cliente de Prisma es código generado que vive en
node_modules/.prisma/client - Editar
schema.prismasolo no regenera el cliente - Ejecuta
npx prisma generatedespués de cada cambio de esquema npx prisma migrate devejecutagenerateautomáticamente
¿Cómo manejo la paginación con Prisma?
const page = 2;
const pageSize = 10;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: "desc" },
}),
prisma.post.count(),
]);Problema: ¿Por qué pasar un campo BigInt a un componente cliente lanza un error?
- Los valores
BigIntno se pueden serializar a JSON - Server Components pasan props a componentes cliente a través de serialización JSON
- Convierte BigInt a
NumberoStringantes de pasar:Number(post.viewCount) - Si el valor excede
Number.MAX_SAFE_INTEGER, usaString()en su lugar
¿Cómo escribo el tipo de un resultado de consulta de Prisma que incluye relaciones en TypeScript?
import { Prisma } from "@prisma/client";
type PostWithAuthor = Prisma.PostGetPayload<{
include: { author: true };
}>;Prisma.PostGetPayloadinfiere la forma exacta basándose eninclude/select- Esto se mantiene sincronizado con tu esquema automáticamente
¿Cómo uso Prisma.PostCreateInput y tipos generados similares?
import { Prisma } from "@prisma/client";
const data: Prisma.PostCreateInput = {
title: "Hello",
author: { connect: { id: 1 } },
};- Prisma genera
*CreateInput,*UpdateInput,*WhereInput, y*OrderByInputpara cada modelo - Estos tipos aplican campos requeridos y operaciones de relaciones válidas en tiempo de compilación
¿Cuál es la diferencia entre prisma migrate dev y prisma db push?
migrate devcrea un archivo de migración SQL, lo aplica y regenera el clientedb pushaplica cambios de esquema directamente sin crear archivos de migración- Usa
migrate devpara flujos de trabajo de producción donde necesitas historial de migración - Usa
db pushpara prototipos rápidos o cuando no necesitas un rastro de migración
¿Cómo escribo una consulta SQL sin procesar con Prisma mientras mantengo seguridad de tipos?
const results = await prisma.$queryRaw<
{ id: number; title: string }[]
>`SELECT id, title FROM "Post" WHERE published = true`;- Usa un literal de plantilla etiquetado para prevenir inyección SQL
- Proporciona un parámetro de tipo genérico para la forma del resultado
- Prisma no valida el genérico contra la consulta real en tiempo de compilación
¿Cómo evito consultas N+1 al renderizar una lista de publicaciones con autores?
- Acceder a
post.authoren un bucle sinincludedispara una consulta por publicación - Usa
include: { author: true }en elfindManyinicial para cargar relaciones con entusiasmo - Alternativamente, usa
selectpara obtener solo los campos específicos de autor que necesitas
Relacionado
- Server Actions de Next.js — Mutaciones con Prisma
- Server Components de Next.js — Obtención de datos con Prisma
- TanStack Query — Almacenamiento en caché del lado del cliente para APIs respaldadas por Prisma
- NextAuth.js — Los adaptadores de base de datos funcionan con Prisma