Migrar la API de películas de consultas SQL en crudo (pg) a Prisma ORM: definir el schema, generar migraciones, reemplazar las consultas del pool de pg por el Prisma Client, y usar transacciones gestionadas por Prisma.
- Haber completado los labs anteriores de w7 (API con auth, PostgreSQL avanzado)
- Haber leído el material del D4 de w7
- PostgreSQL en marcha con la base de datos
peliculas_db
La misma API, pero con:
- Schema en
prisma/schema.prismacon todos los modelos - Migraciones con
prisma migrate dev - Consultas reescritas usando Prisma Client en lugar de
pool.query() - Transacciones con
prisma.$transaction
npm install prisma @prisma/client
npx prisma initEste comando crea:
prisma/schema.prisma— el schema de tu base de datos.envactualizado conDATABASE_URL
Actualiza DATABASE_URL en .env:
DATABASE_URL="postgresql://tu_usuario:tu_password@localhost:5432/peliculas_db?schema=public"Reemplaza el contenido de prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Usuario {
id Int @id @default(autoincrement())
nombre String @db.VarChar(100)
email String @unique @db.VarChar(150)
passwordHash String @map("password_hash") @db.VarChar(255)
rol Rol @default(usuario)
activo Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
favoritos Favorito[]
@@map("usuarios")
}
enum Rol {
usuario
admin
}
model Director {
id Int @id @default(autoincrement())
nombre String @unique @db.VarChar(150)
peliculas Pelicula[]
@@map("directores")
}
model Genero {
id Int @id @default(autoincrement())
nombre String @db.VarChar(100)
slug String @unique @db.VarChar(100)
peliculas Pelicula[]
@@map("generos")
}
model Pelicula {
id Int @id @default(autoincrement())
titulo String @db.VarChar(255)
anio Int?
nota Decimal? @db.Decimal(3, 1)
directorId Int? @map("director_id")
generoId Int? @map("genero_id")
createdAt DateTime @default(now()) @map("created_at")
director Director? @relation(fields: [directorId], references: [id])
genero Genero? @relation(fields: [generoId], references: [id])
resenas Resena[]
favoritos Favorito[]
@@map("peliculas")
}
model Resena {
id Int @id @default(autoincrement())
peliculaId Int @map("pelicula_id")
autor String @db.VarChar(100)
texto String
puntuacion Int
createdAt DateTime @default(now()) @map("created_at")
pelicula Pelicula @relation(fields: [peliculaId], references: [id], onDelete: Cascade)
@@map("resenas")
}
model Favorito {
id Int @id @default(autoincrement())
usuarioId Int @map("usuario_id")
peliculaId Int @map("pelicula_id")
createdAt DateTime @default(now()) @map("created_at")
usuario Usuario @relation(fields: [usuarioId], references: [id], onDelete: Cascade)
pelicula Pelicula @relation(fields: [peliculaId], references: [id], onDelete: Cascade)
@@unique([usuarioId, peliculaId])
@@map("favoritos")
}npx prisma migrate dev --name initEsto:
- Crea
prisma/migrations/TIMESTAMP_init/migration.sql - Aplica la migración a la base de datos
- Genera el Prisma Client en
node_modules/@prisma/client
Verifica en psql que las tablas existen:
\c peliculas_db
\dtCrea src/config/prisma.js:
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error']
})
module.exports = prismaCrea src/controllers/peliculasPrismaController.js (trabajaremos en paralelo con el antiguo hasta migrar todo):
const prisma = require('../config/prisma')
const AppError = require('../utils/AppError')
// GET /api/peliculas
const listarPeliculas = async (req, res, next) => {
try {
const { genero, director, anio, page = 1, limit = 10 } = req.query
const skip = (Number(page) - 1) * Number(limit)
const where = {}
if (genero) where.genero = { slug: genero }
if (director) where.director = { nombre: { contains: director, mode: 'insensitive' } }
if (anio) where.anio = Number(anio)
const [peliculas, total] = await prisma.$transaction([
prisma.pelicula.findMany({
where,
include: {
director: { select: { nombre: true } },
genero: { select: { nombre: true, slug: true } },
_count: { select: { resenas: true } }
},
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit)
}),
prisma.pelicula.count({ where })
])
res.json({
data: peliculas,
total,
pagina: Number(page),
totalPaginas: Math.ceil(total / Number(limit))
})
} catch (err) {
next(err)
}
}
// GET /api/peliculas/:id
const obtenerPelicula = async (req, res, next) => {
try {
const pelicula = await prisma.pelicula.findUnique({
where: { id: Number(req.params.id) },
include: {
director: true,
genero: true,
resenas: {
orderBy: { createdAt: 'desc' },
take: 5
},
_count: { select: { resenas: true, favoritos: true } }
}
})
if (!pelicula) {
throw new AppError('Película no encontrada', 404)
}
res.json(pelicula)
} catch (err) {
next(err)
}
}
// POST /api/peliculas
const crearPelicula = async (req, res, next) => {
try {
const { titulo, anio, nota, director, genero } = req.body
if (!titulo || !anio) {
throw new AppError('titulo y anio son obligatorios', 400)
}
// Buscar o crear director y género en una transacción
const pelicula = await prisma.$transaction(async (tx) => {
let directorId = null
if (director) {
const directorRecord = await tx.director.upsert({
where: { nombre: director },
update: {},
create: { nombre: director }
})
directorId = directorRecord.id
}
let generoId = null
if (genero) {
const generoRecord = await tx.genero.findFirst({
where: {
OR: [
{ slug: genero.toLowerCase() },
{ nombre: { equals: genero, mode: 'insensitive' } }
]
}
})
generoId = generoRecord?.id || null
}
return tx.pelicula.create({
data: {
titulo,
anio: Number(anio),
nota: nota ? Number(nota) : null,
directorId,
generoId
},
include: {
director: true,
genero: true
}
})
})
res.status(201).json(pelicula)
} catch (err) {
next(err)
}
}
// PUT /api/peliculas/:id
const actualizarPelicula = async (req, res, next) => {
try {
const id = Number(req.params.id)
const { titulo, anio, nota, directorId, generoId } = req.body
const existe = await prisma.pelicula.findUnique({ where: { id } })
if (!existe) {
throw new AppError('Película no encontrada', 404)
}
const pelicula = await prisma.pelicula.update({
where: { id },
data: {
titulo,
anio: anio ? Number(anio) : undefined,
nota: nota !== undefined ? (nota ? Number(nota) : null) : undefined,
directorId: directorId ? Number(directorId) : undefined,
generoId: generoId ? Number(generoId) : undefined
},
include: { director: true, genero: true }
})
res.json(pelicula)
} catch (err) {
next(err)
}
}
// DELETE /api/peliculas/:id
const eliminarPelicula = async (req, res, next) => {
try {
const id = Number(req.params.id)
const existe = await prisma.pelicula.findUnique({ where: { id } })
if (!existe) {
throw new AppError('Película no encontrada', 404)
}
await prisma.pelicula.delete({ where: { id } })
res.json({ ok: true, mensaje: 'Película eliminada' })
} catch (err) {
next(err)
}
}
module.exports = { listarPeliculas, obtenerPelicula, crearPelicula, actualizarPelicula, eliminarPelicula }Modifica src/controllers/authController.js para reemplazar pool.query con Prisma:
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const prisma = require('../config/prisma')
const AppError = require('../utils/AppError')
const SALT_ROUNDS = 10
const generarToken = (usuario) =>
jwt.sign(
{ id: usuario.id, email: usuario.email, rol: usuario.rol },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
)
const registro = async (req, res, next) => {
try {
const { nombre, email, password, rol } = req.body
if (!nombre || !email || !password) {
throw new AppError('nombre, email y password son obligatorios', 400)
}
if (password.length < 6) {
throw new AppError('La contraseña debe tener al menos 6 caracteres', 400)
}
const existe = await prisma.usuario.findUnique({ where: { email } })
if (existe) {
throw new AppError('Ya existe un usuario con ese email', 409)
}
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS)
const usuario = await prisma.usuario.create({
data: {
nombre,
email,
passwordHash,
rol: rol === 'admin' ? 'admin' : 'usuario'
},
select: { id: true, nombre: true, email: true, rol: true, createdAt: true }
})
res.status(201).json({ token: generarToken(usuario), usuario })
} catch (err) {
next(err)
}
}
const login = async (req, res, next) => {
try {
const { email, password } = req.body
if (!email || !password) {
throw new AppError('email y password son obligatorios', 400)
}
const usuario = await prisma.usuario.findFirst({
where: { email, activo: true }
})
if (!usuario || !(await bcrypt.compare(password, usuario.passwordHash))) {
throw new AppError('Credenciales incorrectas', 401)
}
res.json({
token: generarToken(usuario),
usuario: { id: usuario.id, nombre: usuario.nombre, email: usuario.email, rol: usuario.rol }
})
} catch (err) {
next(err)
}
}
module.exports = { registro, login }En src/routes/peliculas.js, reemplaza las importaciones:
const {
listarPeliculas,
obtenerPelicula,
crearPelicula,
actualizarPelicula,
eliminarPelicula
} = require('../controllers/peliculasPrismaController')La funcionalidad ha evolucionado: añade un campo destacada a las películas.
Modifica el schema en prisma/schema.prisma:
model Pelicula {
// ... campos existentes ...
destacada Boolean @default(false)
// ...
}Genera la nueva migración:
npx prisma migrate dev --name add_destacada_to_peliculasVerifica en psql:
\d peliculas
-- Debe mostrar la columna 'destacada'Explora la base de datos visualmente:
npx prisma studioAbre http://localhost:5555. Aquí puedes:
- Ver y filtrar registros de cualquier tabla
- Crear y editar registros manualmente
- Explorar relaciones entre modelos
Prueba que todo funciona igual que antes:
# Listar películas con paginación
curl "http://localhost:3000/api/peliculas?page=1&limit=5"
# Filtrar por género
curl "http://localhost:3000/api/peliculas?genero=ciencia-ficcion"
# Crear película (con token de admin)
curl -X POST http://localhost:3000/api/peliculas \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"titulo": "Everything Everywhere All at Once",
"anio": 2022,
"nota": 7.8,
"director": "Daniel Kwan",
"genero": "ciencia-ficcion"
}'
# Obtener película con relaciones e includes
curl http://localhost:3000/api/peliculas/1Responde en NOTAS.md:
-
¿Qué ventajas concretas ofrece Prisma frente a escribir SQL en crudo en este proyecto? Da al menos dos ejemplos específicos.
-
¿Qué hace
prisma.$transaction([query1, query2])? ¿En qué se diferencia deprisma.$transaction(async (tx) => { ... })? -
¿Qué archivo NO deberías commitear nunca al repositorio de tu schema de Prisma? ¿Y cuáles sí deben estar en el repositorio?
-
prisma/schema.prismadefine correctamente los 6 modelos con sus relaciones -
npx prisma migrate devgenera y aplica las migraciones sin errores -
GET /api/peliculasdevuelve películas con director y género incluidos (no IDs) -
GET /api/peliculassoporta paginación con?page=&limit= -
GET /api/peliculas/:idincluye las últimas 5 reseñas y el conteo de favoritos -
POST /api/peliculascrea o reutiliza el director conupsertdentro de una transacción - La segunda migración (campo
destacada) se aplica correctamente -
npx prisma studiomuestra todos los modelos con datos -
POST /api/auth/registroyPOST /api/auth/loginfuncionan con el nuevo controlador Prisma
- Soft delete: En lugar de eliminar películas de la base de datos, añade un campo
deletedAt DateTime?al schema. Modifica la query delistarPeliculaspara que no muestre las películas condeletedAt != null. Crea una migración para este cambio. - Seed con Prisma: Crea
prisma/seed.jsque pobla la base de datos con directores, géneros y películas de prueba. Configura el script enpackage.jsoncon"prisma": { "seed": "node prisma/seed.js" }y ejecútalo connpx prisma db seed. - Relaciones many-to-many: Añade una tabla
pelicula_actoresmany-to-many entrePeliculay un nuevo modeloActor. Genera la migración y actualiza el endpoint de detalle para incluir el reparto.
