Antes de Prisma, trabajar con bases de datos en Node.js significaba escribir SQL a mano o lidiar con ORMs como Sequelize o TypeORM que, aunque potentes, requerían bastante configuración y tenían una experiencia TypeScript mejorable. Prisma cambió eso de raíz.
Lo que hace especial a Prisma no es solo que sea un ORM, sino cómo funciona: defines tu esquema de datos en un archivo .prisma, ejecutas un comando y obtienes un cliente TypeScript completamente tipado que refleja exactamente tu base de datos. Sin decoradores, sin clases abstractas, sin magia. Solo código TypeScript que el compilador puede verificar.
Esta guía cubre la instalación, el modelado de datos con relaciones, las migraciones, las consultas más habituales y los patrones que uso en proyectos reales con PostgreSQL. Al final tendrás una base sólida para usarlo en producción.
Instalación y configuración inicial
Necesitas Node.js 18+ y una base de datos. Para este tutorial uso PostgreSQL, aunque Prisma soporta también MySQL, SQLite, SQL Server y MongoDB.
mkdir proyecto-prisma && cd proyecto-prisma
npm init -y
npm install prisma @prisma/client
npm install -D typescript ts-node @types/node
# Inicializar Prisma (crea prisma/schema.prisma y .env)
npx prisma init --datasource-provider postgresql
Configura la URL de tu base de datos en .env:
DATABASE_URL="postgresql://usuario:contraseña@localhost:5432/mi_proyecto?schema=public"
El schema de Prisma: definir el modelo de datos
El archivo prisma/schema.prisma es el corazón de todo. Define tus modelos, relaciones y configuración de la base de datos con una sintaxis declarativa:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Usuario {
id Int @id @default(autoincrement())
email String @unique
nombre String
rol Rol @default(USER)
creadoEn DateTime @default(now())
// Relación uno-a-muchos con Articulo
articulos Articulo[]
@@map("usuarios") // nombre de la tabla en la DB
}
model Articulo {
id Int @id @default(autoincrement())
titulo String
contenido String
publicado Boolean @default(false)
creadoEn DateTime @default(now())
actualizadoEn DateTime @updatedAt
// Clave foránea
autorId Int
autor Usuario @relation(fields: [autorId], references: [id])
// Relación muchos-a-muchos con Tag
tags Tag[]
@@map("articulos")
}
model Tag {
id Int @id @default(autoincrement())
nombre String @unique
articulos Articulo[]
@@map("tags")
}
enum Rol {
USER
ADMIN
EDITOR
}
Varias cosas a destacar de este schema: @updatedAt actualiza el campo automáticamente en cada UPDATE, @@map permite que el modelo en Prisma tenga un nombre diferente al de la tabla real, y las relaciones muchos-a-muchos se definen de forma implícita sin necesidad de tabla intermedia (Prisma la crea sola).
Migraciones: sincronizar el schema con la base de datos
Prisma tiene un sistema de migraciones que genera SQL a partir de los cambios en tu schema:
# Crear y aplicar una nueva migración
npx prisma migrate dev --name init
# Aplicar migraciones pendientes en producción (sin crear nuevas)
npx prisma migrate deploy
# Resetear la base de datos (¡destruye todos los datos!)
npx prisma migrate reset
# Ver estado de las migraciones
npx prisma migrate status
El comando migrate dev hace tres cosas: genera el SQL de la migración, lo aplica a tu base de datos de desarrollo y regenera el cliente de Prisma. En producción siempre uses migrate deploy, que solo aplica las migraciones pendientes sin generar ni modificar nada.
Prisma también incluye prisma db push para sincronizar el schema directamente sin crear archivos de migración, útil para prototipado rápido, aunque no recomendado para producción.
El Prisma Client: consultas con tipado completo
Una vez que tienes las migraciones aplicadas, instancias el cliente y empiezas a hacer consultas:
// src/database.ts
import { PrismaClient } from '@prisma/client';
// Patrón singleton para evitar múltiples instancias en desarrollo
declare global {
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma;
}
El patrón singleton es importante en desarrollo con hot-reload (Next.js, ts-node-dev) para evitar crear decenas de conexiones a la base de datos.
Consultas CRUD: crear, leer, actualizar y eliminar
Las operaciones básicas con Prisma son muy expresivas. Todo está tipado, así que el IDE te autocompletará los campos disponibles:
import { prisma } from './database';
// CREATE
async function crearUsuario() {
const usuario = await prisma.usuario.create({
data: {
email: 'dev@turincondev.com',
nombre: 'Desarrollador',
rol: 'EDITOR',
},
});
return usuario; // tipo: Usuario (infereido por Prisma)
}
// READ - uno por ID
async function obtenerArticulo(id: number) {
return prisma.articulo.findUnique({
where: { id },
include: {
autor: { select: { nombre: true, email: true } },
tags: true,
},
});
}
// READ - múltiples con filtros
async function listarArticulosPublicados(pagina: number) {
const porPagina = 10;
return prisma.articulo.findMany({
where: { publicado: true },
orderBy: { creadoEn: 'desc' },
skip: (pagina - 1) * porPagina,
take: porPagina,
include: { autor: { select: { nombre: true } }, tags: true },
});
}
// UPDATE
async function publicarArticulo(id: number) {
return prisma.articulo.update({
where: { id },
data: { publicado: true },
});
}
// DELETE
async function eliminarArticulo(id: number) {
return prisma.articulo.delete({ where: { id } });
}
Relaciones: crear y conectar datos relacionados
Prisma maneja las relaciones de forma elegante. Puedes crear datos relacionados en una sola operación o conectar registros existentes:
// Crear artículo con tags (conectando tags existentes o creando nuevos)
async function crearArticuloConTags(autorId: number) {
return prisma.articulo.create({
data: {
titulo: 'Guía de Prisma ORM',
contenido: 'Contenido del artículo...',
autorId,
tags: {
connectOrCreate: [
{ where: { nombre: 'TypeScript' }, create: { nombre: 'TypeScript' } },
{ where: { nombre: 'Node.js' }, create: { nombre: 'Node.js' } },
],
},
},
include: { tags: true },
});
}
// Actualizar relación muchos-a-muchos
async function actualizarTagsArticulo(articuloId: number, nuevosTags: string[]) {
return prisma.articulo.update({
where: { id: articuloId },
data: {
tags: {
set: [], // desconectar todos los tags actuales
connectOrCreate: nuevosTags.map(nombre => ({
where: { nombre },
create: { nombre },
})),
},
},
include: { tags: true },
});
}
Transacciones para operaciones atómicas
Cuando necesitas que múltiples operaciones se ejecuten de forma atómica (todas o ninguna), Prisma ofrece dos tipos de transacciones:
// Transacción secuencial: las operaciones dependen unas de otras
async function transferirCreditos(deUsuarioId: number, aUsuarioId: number, cantidad: number) {
return prisma.$transaction(async (tx) => {
// Verificar saldo
const origen = await tx.usuario.findUniqueOrThrow({ where: { id: deUsuarioId } });
if ((origen as any).creditos < cantidad) {
throw new Error('Saldo insuficiente');
}
// Descontar del origen
await tx.usuario.update({
where: { id: deUsuarioId },
data: { creditos: { decrement: cantidad } } as any,
});
// Añadir al destino
await tx.usuario.update({
where: { id: aUsuarioId },
data: { creditos: { increment: cantidad } } as any,
});
});
}
// Transacción batch: operaciones independientes, más eficiente
async function operacionesBatch() {
const [nuevoUsuario, articulosActualizados] = await prisma.$transaction([
prisma.usuario.create({ data: { email: 'nuevo@email.com', nombre: 'Nuevo' } }),
prisma.articulo.updateMany({ where: { publicado: false }, data: { publicado: true } }),
]);
return { nuevoUsuario, articulosActualizados };
}
Consultas avanzadas: agregaciones y groupBy
Prisma soporta operaciones de agregación directamente desde el cliente, sin necesidad de SQL crudo:
// Contar, sumar, promedio, mínimo, máximo
async function estadisticas() {
const stats = await prisma.articulo.aggregate({
_count: { id: true },
where: { publicado: true },
});
console.log(`Artículos publicados: ${stats._count.id}`);
}
// Agrupar por campo
async function articulosPorAutor() {
return prisma.articulo.groupBy({
by: ['autorId'],
_count: { id: true },
orderBy: { _count: { id: 'desc' } },
take: 10,
});
}
// SQL crudo cuando lo necesitas
async function busquedaFullText(query: string) {
return prisma.$queryRaw`
SELECT id, titulo, ts_rank(search_vector, query) AS rank
FROM articulos, plainto_tsquery('spanish', ${query}) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20
`;
}
Prisma en producción: configuración y rendimiento
En producción hay varios aspectos de configuración importantes. El primero es el connection pooling. Prisma no gestiona un pool de conexiones por defecto en entornos serverless; para eso necesitas Prisma Accelerate o pgBouncer:
// Para aplicaciones long-running (Express, Fastify)
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
// Configurar límites del pool
const prismaWithPool = new PrismaClient({
datasourceUrl: `${process.env.DATABASE_URL}?connection_limit=10&pool_timeout=20`,
});
// En serverless/Edge (Vercel, Cloudflare Workers): usar Prisma Accelerate
// O conectar via pgBouncer para reutilizar conexiones
Otro aspecto crucial es el logging de queries lentas. En producción activa solo el log de errores y usa las métricas de tu proveedor de base de datos para detectar queries problemáticas. Nunca actives log: ['query'] en producción porque genera demasiado output.
Prisma Studio: explorar la base de datos visualmente
Prisma incluye una herramienta visual que no se menciona suficiente: Prisma Studio. Es un explorador de base de datos que se abre en el navegador y te permite ver, crear, editar y eliminar registros sin tocar SQL:
npx prisma studio
# Abre http://localhost:5555
Para el día a día de desarrollo es un ahorro de tiempo considerable. Ver las relaciones entre registros, verificar que las migraciones se aplicaron correctamente, o hacer correcciones puntuales de datos, todo desde una interfaz limpia sin necesidad de un cliente SQL externo.
Siguiente paso
Si tienes un proyecto Node.js con TypeScript que ya usa una base de datos relacional, el mejor primer paso con Prisma es hacer una migración de introspección: npx prisma db pull genera el schema de Prisma a partir de tu base de datos existente. Desde ahí puedes empezar a usar el cliente en rutas nuevas sin tocar el código antiguo.
El mayor salto de calidad que notarás es en el autocomplete y la verificación de tipos. Cuando cambias el nombre de un campo en el schema y Prisma te muestra exactamente en qué partes del código lo usas, te preguntas cómo pudiste trabajar sin eso antes.