Prisma es el ORM más popular del ecosistema Node.js y TypeScript en 2026. A diferencia de ORMs más tradicionales como Sequelize o TypeORM, Prisma adopta un enfoque centrado en el esquema: defines tus modelos en un archivo schema.prisma, y Prisma genera automáticamente un cliente con autocompletado completo y tipado estricto. El resultado es que trabajar con la base de datos desde TypeScript se siente casi tan seguro como trabajar con datos locales.
En esta guía aprenderás a instalar Prisma, definir tu esquema, hacer migraciones y escribir consultas reales para los casos de uso más habituales.
Instalación y configuración inicial
# Crear un proyecto Node.js con TypeScript
mkdir mi-proyecto && cd mi-proyecto
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init
# Instalar Prisma
npm install prisma --save-dev
npm install @prisma/client
# Inicializar Prisma (crea el archivo schema.prisma y el .env)
npx prisma init --datasource-provider postgresql
# Para MySQL: npx prisma init --datasource-provider mysql
# Para SQLite: npx prisma init --datasource-provider sqlite
Después de ejecutar prisma init, se crea la siguiente estructura:
mi-proyecto/
├── prisma/
│ └── schema.prisma ← definición del esquema y modelos
├── .env ← cadena de conexión a la base de datos
├── package.json
└── tsconfig.json
Configura la cadena de conexión en el archivo .env:
# .env
DATABASE_URL="postgresql://usuario:contraseña@localhost:5432/mi_base_datos"
# Para MySQL:
# DATABASE_URL="mysql://usuario:contraseña@localhost:3306/mi_base_datos"
# Para SQLite (sin servidor, ideal para desarrollo):
# DATABASE_URL="file:./dev.db"
Definir el esquema
El archivo schema.prisma es el centro de Prisma. Aquí defines los modelos (tablas), sus campos (columnas), tipos, relaciones y restricciones. La sintaxis es clara y mucho más concisa que escribir SQL a mano.
// 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
bio String? // ? = campo opcional (puede ser null)
activo Boolean @default(true)
creado_en DateTime @default(now())
actualizado DateTime @updatedAt // se actualiza automáticamente
// Relaciones
publicaciones Publicacion[]
perfil Perfil? // relación uno a uno (opcional)
@@map("usuarios") // nombre de la tabla en la BD (si difiere del modelo)
}
model Perfil {
id Int @id @default(autoincrement())
avatar String?
sitio_web String?
ubicacion String?
// Relación uno a uno con Usuario
usuario Usuario @relation(fields: [usuario_id], references: [id], onDelete: Cascade)
usuario_id Int @unique
@@map("perfiles")
}
model Publicacion {
id Int @id @default(autoincrement())
titulo String
contenido String
publicado Boolean @default(false)
creado_en DateTime @default(now())
autor Usuario @relation(fields: [autor_id], references: [id])
autor_id Int
categoria Categoria @relation(fields: [categoria_id], references: [id])
categoria_id Int
etiquetas Etiqueta[]
comentarios Comentario[]
// Índice compuesto para búsquedas frecuentes
@@index([autor_id, publicado])
@@index([categoria_id])
@@map("publicaciones")
}
model Categoria {
id Int @id @default(autoincrement())
nombre String @unique
publicaciones Publicacion[]
@@map("categorias")
}
model Etiqueta {
id Int @id @default(autoincrement())
nombre String @unique
publicaciones Publicacion[] // relación muchos a muchos (Prisma gestiona la tabla intermedia)
@@map("etiquetas")
}
model Comentario {
id Int @id @default(autoincrement())
texto String
creado_en DateTime @default(now())
publicacion Publicacion @relation(fields: [publicacion_id], references: [id], onDelete: Cascade)
publicacion_id Int
@@map("comentarios")
}
Migraciones: llevar el esquema a la base de datos
# Crear y aplicar una migración (en desarrollo)
# Prisma genera el SQL necesario a partir del schema.prisma
npx prisma migrate dev --name crear_esquema_inicial
# Prisma crea el archivo de migración en:
# prisma/migrations/20260615120000_crear_esquema_inicial/migration.sql
# y lo aplica a la base de datos inmediatamente
# Ver el SQL que generó Prisma
cat prisma/migrations/20260615120000_crear_esquema_inicial/migration.sql
# Cuando modificas el schema.prisma y quieres aplicar los cambios:
# 1. Haz los cambios en schema.prisma (añadir campo, modelo, índice...)
# 2. Ejecutar la migración
npx prisma migrate dev --name anadir_campo_bio_usuario
# En producción (no modifica el schema, solo aplica migraciones pendientes):
npx prisma migrate deploy
# Ver el estado de las migraciones
npx prisma migrate status
# Si el esquema y la BD están desincronizados (solo para desarrollo):
npx prisma migrate reset # CUIDADO: borra y recrea toda la BD
# Generar el cliente Prisma después de cambios en el schema
# (migrate dev lo hace automáticamente, pero a veces hay que hacerlo a mano)
npx prisma generate
# Abrir Prisma Studio: interfaz visual para explorar y editar los datos
npx prisma studio
Inicializar el cliente Prisma
// src/db.ts — instancia singleton del cliente Prisma
import { PrismaClient } from '@prisma/client';
// En producción es importante usar un singleton para no abrir demasiadas conexiones
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query', 'error', 'warn'], // mostrar las queries en desarrollo
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
Operaciones CRUD básicas
import { prisma } from './db';
// ─── CREATE ───────────────────────────────────────────────────────────────────
// Crear un usuario
const nuevoUsuario = await prisma.usuario.create({
data: {
email: 'ana@mail.com',
nombre: 'Ana García',
bio: 'Desarrolladora full-stack apasionada por TypeScript',
},
});
console.log(nuevoUsuario.id); // número autoincremental asignado por la BD
// Crear con relación anidada: usuario + perfil en una sola operación
const usuarioConPerfil = await prisma.usuario.create({
data: {
email: 'carlos@mail.com',
nombre: 'Carlos López',
perfil: {
create: {
avatar: 'https://cdn.ejemplo.com/avatars/carlos.jpg',
ubicacion: 'Madrid',
},
},
},
include: { perfil: true }, // incluir el perfil en la respuesta
});
// Crear muchos registros de una vez (más eficiente que varios create)
const { count } = await prisma.categoria.createMany({
data: [
{ nombre: 'Tecnología' },
{ nombre: 'Diseño' },
{ nombre: 'Negocios' },
{ nombre: 'Ciencia' },
],
skipDuplicates: true, // ignorar si ya existe (no falla)
});
console.log(`Creadas ${count} categorías`);
// ─── READ ─────────────────────────────────────────────────────────────────────
// Buscar por clave primaria (findUnique requiere campo único)
const usuario = await prisma.usuario.findUnique({
where: { id: 1 },
});
// Buscar por campo único
const porEmail = await prisma.usuario.findUnique({
where: { email: 'ana@mail.com' },
});
// findFirst: primera coincidencia (no requiere campo único)
const primerActivo = await prisma.usuario.findFirst({
where: { activo: true },
orderBy: { creado_en: 'desc' },
});
// findMany: todos los que coincidan con los filtros
const usuariosActivos = await prisma.usuario.findMany({
where: { activo: true },
orderBy: { nombre: 'asc' },
take: 10, // equivale a LIMIT
skip: 20, // equivale a OFFSET (para paginación)
});
// Seleccionar solo los campos necesarios (evitar over-fetching)
const nombres = await prisma.usuario.findMany({
select: {
id: true,
nombre: true,
email: true,
// activo, bio, etc. no se incluyen → más eficiente
},
});
// Incluir relaciones
const usuarioConPublicaciones = await prisma.usuario.findUnique({
where: { id: 1 },
include: {
publicaciones: {
where: { publicado: true },
orderBy: { creado_en: 'desc' },
take: 5,
include: {
categoria: true,
etiquetas: true,
},
},
perfil: true,
},
});
// ─── UPDATE ───────────────────────────────────────────────────────────────────
// Actualizar un registro por clave única
const actualizado = await prisma.usuario.update({
where: { id: 1 },
data: { bio: 'Bio actualizada', activo: false },
});
// Actualizar muchos registros a la vez
const { count: desactivados } = await prisma.usuario.updateMany({
where: {
activo: true,
creado_en: { lt: new Date('2023-01-01') } // creados antes de 2023
},
data: { activo: false },
});
console.log(`${desactivados} usuarios desactivados`);
// Upsert: crear si no existe, actualizar si existe
const upserted = await prisma.usuario.upsert({
where: { email: 'nuevo@mail.com' },
update: { nombre: 'Nombre actualizado' },
create: {
email: 'nuevo@mail.com',
nombre: 'Usuario Nuevo',
},
});
// ─── DELETE ───────────────────────────────────────────────────────────────────
// Eliminar por clave única
await prisma.usuario.delete({
where: { id: 1 },
});
// Eliminar muchos registros
const { count: eliminados } = await prisma.publicacion.deleteMany({
where: {
publicado: false,
creado_en: { lt: new Date('2024-01-01') } // borradores antiguos
},
});
console.log(`${eliminados} borradores eliminados`);
Filtros avanzados
import { prisma } from './db';
// Operadores de comparación
const publicaciones = await prisma.publicacion.findMany({
where: {
// Igualdad (por defecto)
publicado: true,
// Comparaciones numéricas/de fecha
creado_en: { gte: new Date('2026-01-01') }, // >= (greater than or equal)
// En una lista de valores
categoria_id: { in: [1, 2, 3] },
// Búsqueda de texto (LIKE)
titulo: { contains: 'TypeScript', mode: 'insensitive' }, // insensible a mayúsculas
// Empieza por
titulo: { startsWith: 'Cómo' },
// NOT: negación
autor_id: { not: 5 },
}
});
// Operadores lógicos AND, OR, NOT
const resultados = await prisma.publicacion.findMany({
where: {
AND: [
{ publicado: true },
{ creado_en: { gte: new Date('2026-01-01') } }
]
}
});
const resultadosOR = await prisma.publicacion.findMany({
where: {
OR: [
{ titulo: { contains: 'React' } },
{ contenido: { contains: 'React' } }
]
}
});
// Filtrar por relaciones
const publicacionesDeAna = await prisma.publicacion.findMany({
where: {
autor: {
email: 'ana@mail.com' // filtrar por campo del modelo relacionado
}
}
});
// some / every / none: filtros en relaciones "a muchos"
const usuariosConPublicaciones = await prisma.usuario.findMany({
where: {
publicaciones: {
some: {
publicado: true // tiene AL MENOS UNA publicación publicada
}
}
}
});
const usuariosSinPublicaciones = await prisma.usuario.findMany({
where: {
publicaciones: {
none: {} // no tiene ninguna publicación
}
}
});
Agregaciones y consultas analíticas
// Contar
const totalUsuarios = await prisma.usuario.count();
const totalActivos = await prisma.usuario.count({ where: { activo: true } });
// Agregaciones numéricas
const estadisticas = await prisma.publicacion.aggregate({
_count: { id: true }, // número de publicaciones
_avg: { categoria_id: true }, // no muy útil, solo para ilustrar
where: { publicado: true }
});
// Group by: agrupar y agregar
const publicacionesPorCategoria = await prisma.publicacion.groupBy({
by: ['categoria_id'],
_count: { id: true },
_min: { creado_en: true },
_max: { creado_en: true },
where: { publicado: true },
orderBy: { _count: { id: 'desc' } }, // ordenar por número de publicaciones
});
console.log(publicacionesPorCategoria);
// [
// { categoria_id: 1, _count: { id: 42 }, _min: { creado_en: ... }, _max: { ... } },
// { categoria_id: 3, _count: { id: 28 }, ... },
// ...
// ]
Transacciones
// Transacción secuencial: todas las operaciones en la misma transacción
// Si alguna falla, todas se revierten automáticamente
const [nuevaPublicacion, usuarioActualizado] = await prisma.$transaction([
prisma.publicacion.create({
data: {
titulo: 'Mi nueva publicación',
contenido: 'Contenido aquí...',
autor_id: 1,
categoria_id: 2,
}
}),
prisma.usuario.update({
where: { id: 1 },
data: { actualizado: new Date() }
})
]);
// Transacción interactiva: más flexible, permite lógica condicional
const resultado = await prisma.$transaction(async (tx) => {
// Verificar que el usuario existe
const usuario = await tx.usuario.findUnique({ where: { id: 1 } });
if (!usuario) throw new Error('Usuario no encontrado');
// Contar sus publicaciones actuales
const numPublicaciones = await tx.publicacion.count({
where: { autor_id: 1 }
});
if (numPublicaciones >= 100) {
throw new Error('Has alcanzado el límite de publicaciones');
}
// Crear la publicación
const publicacion = await tx.publicacion.create({
data: {
titulo: 'Publicación con validación',
contenido: '...',
autor_id: 1,
categoria_id: 1,
}
});
return { publicacion, total: numPublicaciones + 1 };
// Si cualquier operación falla, el tx hace rollback automático
});
SQL raw: cuando Prisma no llega
// Para consultas muy específicas que el API de Prisma no puede expresar,
// puedes usar SQL raw directamente
// queryRaw: ejecutar SQL y recibir los resultados tipados
const resultado = await prisma.$queryRaw<{ nombre: string; total: number }[]>`
SELECT u.nombre, COUNT(p.id)::int AS total
FROM usuarios u
LEFT JOIN publicaciones p ON p.autor_id = u.id
WHERE u.activo = true
GROUP BY u.id, u.nombre
ORDER BY total DESC
LIMIT 10
`;
resultado.forEach(r => console.log(`${r.nombre}: ${r.total} publicaciones`));
// executeRaw: ejecutar SQL sin retorno de datos (INSERT, UPDATE, DELETE)
const filas = await prisma.$executeRaw`
UPDATE publicaciones
SET publicado = true
WHERE creado_en < ${new Date('2026-01-01')}
AND publicado = false
`;
console.log(`${filas} publicaciones marcadas como publicadas`);
// IMPORTANTE: usar template literals con ${}
// para que Prisma parametrice automáticamente los valores y prevenga SQL injection
// NUNCA: prisma.$queryRaw(`SELECT * FROM usuarios WHERE id = ${id}`) ← vulnerable
// SIEMPRE: prisma.$queryRaw`SELECT * FROM usuarios WHERE id = ${id}` ← seguro
Prisma con Express: ejemplo completo de API REST
// src/index.ts
import express from 'express';
import { prisma } from './db';
const app = express();
app.use(express.json());
// GET /usuarios — listar con paginación
app.get('/usuarios', async (req, res) => {
const pagina = Number(req.query.pagina) || 1;
const limite = Number(req.query.limite) || 10;
const skip = (pagina - 1) * limite;
const [usuarios, total] = await prisma.$transaction([
prisma.usuario.findMany({
where: { activo: true },
select: { id: true, nombre: true, email: true, creado_en: true },
orderBy: { nombre: 'asc' },
skip,
take: limite,
}),
prisma.usuario.count({ where: { activo: true } }),
]);
res.json({
datos: usuarios,
total,
pagina,
total_paginas: Math.ceil(total / limite),
});
});
// GET /usuarios/:id — obtener uno con sus publicaciones
app.get('/usuarios/:id', async (req, res) => {
const usuario = await prisma.usuario.findUnique({
where: { id: Number(req.params.id) },
include: {
perfil: true,
publicaciones: {
where: { publicado: true },
orderBy: { creado_en: 'desc' },
take: 5,
select: { id: true, titulo: true, creado_en: true },
},
},
});
if (!usuario) return res.status(404).json({ error: 'Usuario no encontrado' });
res.json(usuario);
});
// POST /usuarios — crear usuario
app.post('/usuarios', async (req, res) => {
const { email, nombre, bio } = req.body;
try {
const usuario = await prisma.usuario.create({
data: { email, nombre, bio },
});
res.status(201).json(usuario);
} catch (error: any) {
if (error.code === 'P2002') { // código Prisma para unique constraint violation
return res.status(409).json({ error: 'El email ya está registrado' });
}
res.status(500).json({ error: 'Error interno del servidor' });
}
});
// PATCH /usuarios/:id — actualizar parcialmente
app.patch('/usuarios/:id', async (req, res) => {
try {
const usuario = await prisma.usuario.update({
where: { id: Number(req.params.id) },
data: req.body,
select: { id: true, nombre: true, email: true, bio: true },
});
res.json(usuario);
} catch (error: any) {
if (error.code === 'P2025') { // registro no encontrado
return res.status(404).json({ error: 'Usuario no encontrado' });
}
res.status(500).json({ error: 'Error interno del servidor' });
}
});
// DELETE /usuarios/:id — desactivar (soft delete)
app.delete('/usuarios/:id', async (req, res) => {
try {
await prisma.usuario.update({
where: { id: Number(req.params.id) },
data: { activo: false },
});
res.status(204).send();
} catch {
res.status(404).json({ error: 'Usuario no encontrado' });
}
});
app.listen(3000, () => console.log('API en http://localhost:3000'));
Códigos de error más comunes de Prisma
| Código | Significado | Cuándo ocurre |
|---|---|---|
P2002 | Unique constraint violation | Intentar insertar un valor duplicado en un campo @unique |
P2025 | Record not found | update, delete o findUniqueOrThrow sobre un registro que no existe |
P2003 | Foreign key constraint failed | Insertar o actualizar con una clave foránea que no existe en la tabla referenciada |
P2014 | Relation violation | Romper una relación requerida |
P1001 | Connection refused | No se puede conectar al servidor de base de datos |
Resumen
- Prisma gira alrededor del archivo
schema.prismadonde defines tus modelos, tipos, relaciones e índices. El cliente TypeScript se genera automáticamente con tipado completo. - Las migraciones se gestionan con
prisma migrate dev(desarrollo) yprisma migrate deploy(producción). Prisma genera el SQL por ti y mantiene un historial versionado. - El cliente ofrece métodos para todas las operaciones CRUD:
create,createMany,findUnique,findFirst,findMany,update,updateMany,upsert,delete,deleteMany. - Los filtros cubren comparaciones (
gte,lt,in), texto (contains,startsWith), lógica (AND,OR,NOT) y relaciones (some,every,none). selectpermite pedir solo los campos necesarios;includepermite cargar relaciones anidadas en la misma consulta.- Las transacciones se hacen con
$transaction([])para operaciones paralelas o$transaction(async tx => ...)para lógica condicional con rollback automático. - Para consultas que el API no puede expresar,
$queryRawy$executeRawpermiten SQL directo con parametrización automática para prevenir inyección SQL. - Los errores más habituales tienen códigos específicos (
P2002para duplicados,P2025para registros no encontrados) que puedes capturar y tratar de forma diferenciada.
No hay comentarios todavía. Sé el primero en compartir tu opinión.