Sesión 1 — Fundamentos de ORMs + Prisma
Bloque: ORMs, APIs REST y Seguridad Fecha: Lunes 23 de noviembre · 15:30–19:30 (4h) Stack: Node.js ≥ 24, TypeScript, Prisma 7, PostgreSQL 16, Docker
Objetivos de la sesión
Sección titulada «Objetivos de la sesión»Al terminar, el alumno debe ser capaz de:
- Explicar qué es un ORM y justificar cuándo NO usarlo.
- Leer y escribir el TypeScript mínimo del bloque: tipos,
async/awaite imports con.js. - Definir modelos en
schema.prisma: campos, tipos,@id,@uniquey enums. - Modelar una relación 1:N con sus dos lados.
- Aplicar cambios de schema con
migrate dev+generatey leer el SQL generado. - Hacer CRUD con Prisma Client, revisando el código que genera una IA.
Proyecto guía del bloque: vamos a construir AcademIA, una plataforma simple de cursos online (usuarios, cursos, inscripciones). Este mismo dominio se usará en las 5 sesiones del bloque y en la sesión de testing, así que lo que se monte hoy se reutiliza hasta diciembre.
Requisitos previos / setup antes de clase
Sección titulada «Requisitos previos / setup antes de clase»- Node.js ≥ 24 instalado → comprobad
node --version - Docker Desktop instalado y arrancando → comprobad
docker --version - Descargad ya la imagen de PostgreSQL (pesa, mejor en casa que con la wifi de clase):
docker pull postgres:16 - git instalado. En Windows, usad Git Bash para todo el bloque: los comandos del curso usan
&&,#yrm, que no funcionan encmdni en Windows PowerShell 5.1 - Acceso al repo
academia-api(GitHub) o el zip de la ramas1-startdescargado del campus - Extensión de VS Code: Prisma (
Prisma.prisma) - Cliente de DB opcional para inspección visual: TablePlus / DBeaver / pgAdmin (no obligatorio)
1. Bienvenida, proyecto guía y punto de partida
Sección titulada «1. Bienvenida, proyecto guía y punto de partida»Hasta ahora en el módulo de fundamentos habéis trabajado con SQL directo (SELECT, INSERT, joins a mano) y con MongoDB (driver nativo o Mongoose). Hoy damos un paso más: en el mundo real, la mayoría de backends en Node no escriben SQL a mano para el día a día — usan un ORM.
Ejemplo rápido para poner en contexto (no se ejecuta, solo se muestra):
-- Sin ORM: SQL a manoSELECT * FROM "Course" WHERE "published" = true;// Con ORM: Prismaconst published = await prisma.course.findMany({ where: { published: true } });Misma información, pero la segunda versión es type-safe, autocompletada por el editor, y si cambias el schema, TypeScript te avisa de lo que rompes.
Vuestro punto de partida: el repo academia-api
Sección titulada «Vuestro punto de partida: el repo academia-api»Clonad el repo de referencia en la rama s1-start (solo tiene un README) y cread vuestra propia rama de trabajo:
git clone -b s1-start \ https://github.com/EmilioCarrion/academia-api-alumnos.git academia-apicd academia-apigit switch -c mi-trabajoSin git: descargad el zip de s1-start (https://github.com/EmilioCarrion/academia-api-alumnos/archive/refs/heads/s1-start.zip), descomprimidlo como academia-api, entrad en la carpeta y haced git init && git add . && git commit -m "s1-start".
En Windows, todo desde Git Bash.
Bloques para copiar: web del curso → Sesión 1 → Chuleta. Cada bloque lleva un ID (📋 S1·07) que veréis en la slide; lo que la slide marca ✍️ se teclea a mano.
2. ¿Qué es un ORM y cuándo NO usarlo?
Sección titulada «2. ¿Qué es un ORM y cuándo NO usarlo?»ORM = Object-Relational Mapping. Es una capa que traduce entre:
- Tu código (objetos/clases en JS/TS)
- Las filas y tablas de una base de datos relacional
En lugar de escribir SQL, describes tus datos como modelos y el ORM genera las queries por ti.
Ventajas
Sección titulada «Ventajas»- Productividad: menos código repetitivo (boilerplate) para CRUD.
- Type-safety: con Prisma, el cliente generado conoce tu schema — autocompletado y errores en tiempo de compilación.
- Migraciones versionadas: los cambios de schema quedan en el historial de git, no en la cabeza de alguien.
- Portabilidad relativa: cambiar de PostgreSQL a MySQL es (en teoría) un cambio de configuración, no reescribir todas las queries.
- Protección contra inyección SQL por defecto (parametrización automática).
Desventajas / cuándo NO usar un ORM
Sección titulada «Desventajas / cuándo NO usar un ORM»- Queries muy complejas (reportes, agregaciones pesadas, window functions) — a veces el SQL generado es ineficiente y toca bajar a SQL raw.
- Curva de abstracción: si no entiendes qué SQL genera tu ORM por debajo, puedes tener sorpresas de rendimiento (el clásico problema N+1).
- Overhead en proyectos muy pequeños o scripts puntuales, donde un
pgclient a pelo es más simple.
Para pensar: “¿Un ORM os protege de inyección SQL al 100%?”
Mención: panorama de ORMs/query builders en Node
Sección titulada «Mención: panorama de ORMs/query builders en Node»Para situaros en el mercado, no hay que memorizarlo:
| Herramienta | Tipo | Puntos fuertes | Cuándo se usa |
|---|---|---|---|
| Prisma | ORM | Type-safety excelente, DX moderna, migraciones declarativas | Estándar de facto en proyectos Node/TS nuevos |
| Sequelize | ORM | Maduro, muy extendido, más flexible en runtime | Proyectos legacy, equipos que vienen de JS puro |
| TypeORM | ORM | Basado en decorators, estilo similar a Hibernate/Java | Equipos con background OOP/Java |
| Drizzle | Query builder type-safe | Muy ligero, SQL-like, control fino | Proyectos que quieren type-safety sin “magia” |
| Knex | Query builder | Solo construcción de queries, sin modelado | Cuando quieres control total del SQL |
| Mongoose | ODM (no relacional) | Modelos y validación sobre MongoDB | Bases de datos documentales |
Por qué Prisma en este curso: es de los más pedidos en ofertas Node/TS, el schema declarativo + cliente autogenerado reduce fricción para aprender el concepto de ORM, y las migraciones son de primera clase.
3. Setup: PostgreSQL + Prisma 7
Sección titulada «3. Setup: PostgreSQL + Prisma 7»3.1 Convertir el repo en un proyecto Node
Sección titulada «3.1 Convertir el repo en un proyecto Node»Dentro de academia-api (el repo que acabáis de clonar), convertidlo en un proyecto Node e instalad las dependencias (§3.2 explica cada una):
npm init -ynpm pkg set type=modulenpm i -D prisma@7 typescript tsx @types/nodenpm i @prisma/client@7 @prisma/adapter-pg pg dotenv"type": "module" es obligatorio: Prisma 7 genera el cliente como ES modules.
3.2 Las dependencias (Prisma 7 fijado)
Sección titulada «3.2 Las dependencias (Prisma 7 fijado)»prisma(CLI) y@prisma/clientfijados a la 7. Sin el@7, hoynpminstala la 8, que está en release candidate.typescript+@types/node: tipos.tsx: ejecuta.tsdirectamente, sin compilar.@prisma/adapter-pg+pg: en Prisma 7 el cliente se conecta a Postgres a través de un driver adapter (la pieza que traduce las queries de Prisma al driverpg), es obligatorio.- npm mostrará avisos de
allow-scripts: no son errores, seguid. dotenv: Prisma 7 ya no carga el.envsolo.
3.3 Levantar PostgreSQL con Docker Compose
Sección titulada «3.3 Levantar PostgreSQL con Docker Compose»Crear docker-compose.yml en la raíz del proyecto:
# docker-compose.yml (en la raíz del proyecto)services: db: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: academia POSTGRES_PASSWORD: academia POSTGRES_DB: academia ports: - "5432:5432" volumes: - academia_pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U academia"] interval: 5s timeout: 3s retries: 10
volumes: academia_pgdata:docker compose up -ddocker compose ps# esperad a ver (healthy) en STATUSEl healthcheck con pg_isready es lo que hace que docker compose ps diga healthy: sin él solo sabes que el contenedor arrancó, no que Postgres acepta conexiones.
3.4 Plan B: si Docker no arranca
Sección titulada «3.4 Plan B: si Docker no arranca»Prisma 7 trae un Postgres local que no necesita Docker:
npx prisma dev --name academia --detachLa primera vez descarga lo que necesita (hace falta internet). Usa el prisma que acabáis de instalar (§3.2): por eso las dependencias van antes. Copiad la URL postgres:// que imprime, algo como:
postgres://postgres:postgres@localhost:51214/template1?sslmode=disableSi la perdéis, npx prisma dev ls la vuelve a mostrar: es la línea TCP, no la DATABASE_URL: prisma+postgres://… que sale primero.
Esa URL es vuestro DATABASE_URL en .env (§3.5), en lugar de la de Docker. Con ella funcionan migraciones, seed, CRUD y todo lo que haremos en el bloque, hasta los tests de la sesión 6. La única excepción es la demo de carrera de la sesión 2: este Postgres serializa las conexiones y la carrera no se reproduce (la veréis en la pantalla del profesor).
3.5 Inicializar Prisma
Sección titulada «3.5 Inicializar Prisma»npx prisma init --datasource-provider postgresqlEsto genera:
prisma/ schema.prisma ← modelosprisma7.config.ts ← configuración de Prisma (incluida la URL de la DB).env ← DATABASE_URL.gitignore.claude/ .agents/ .windsurf/ skills-lock.json ← skills para agentes de IA (ver abajo)En Prisma 7 el cliente se generará en generated/prisma, dentro de vuestro proyecto, no en node_modules (lo vemos en §5 y §6).
Regalo de prisma init: skills para agentes. Prisma 7 instala skills para asistentes de IA (.claude/, .agents/, .windsurf/, skills-lock.json): instrucciones que leerá vuestro agente cuando trabaje en este repo. Son instrucciones que otro ha escrito por vosotros: miradlas antes de commitearlas. El repo de referencia no las lleva, así que antes del primer commit borradlas:
rm -rf .claude .agents .windsurf skills-lock.jsonSi las queréis conservar en local, añadidlas al .gitignore. Si no hacéis ninguna de las dos cosas, el git add . de §3.6 sube unos 100 ficheros de skills.
En .env (y copia el mismo contenido a .env.example, que sí se sube a git como plantilla):
# .env (y el mismo contenido en .env.example)DATABASE_URL="postgresql://academia:academia@localhost:5432/academia"Con el plan B, en .env va la URL postgres:// que imprimió npx prisma dev --detach; .env.example se queda con la de Docker.
prisma7.config.ts (generado; no hace falta tocarlo):
import "dotenv/config";import { defineConfig } from "prisma/config";
export default defineConfig({ schema: "prisma/schema.prisma", migrations: { path: "prisma/migrations", }, datasource: { url: process.env["DATABASE_URL"], },});3.6 .gitignore y primer commit
Sección titulada «3.6 .gitignore y primer commit»Comprobad que el .gitignore generado contiene al menos estas líneas (si falta alguna, añadidla):
# .gitignore (al menos estas líneas)node_modules.env/generated/prismacat .gitignoregit add .git status # ni .env ni .claude/ deben aparecergit commit -m "Setup inicial: Postgres + Prisma 7"4. TypeScript mínimo
Sección titulada «4. TypeScript mínimo»Lo justo para leer y escribir el código de este bloque. Configuración (tsconfig.json en la raíz):
// tsconfig.json (en la raíz del proyecto){ "compilerOptions": { "target": "ES2023", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "verbatimModuleSyntax": true, "noEmit": true, "types": ["node"] }, "include": ["src", "prisma7.config.ts"]}"types": ["node"] carga los tipos de Node (process, Buffer…). Sin él, npm run typecheck falla con Cannot find name 'process' en prisma7.config.ts mientras no exista src/.
Y un script en package.json para comprobar tipos (el comando lo añade dentro de "scripts" sin que tengáis que editar el JSON):
npm pkg set scripts.typecheck="tsc --noEmit"En package.json queda "typecheck": "tsc --noEmit" dentro de "scripts".
Cinco ideas que vais a ver hoy:
- Anotaciones de tipo:
name: string,id: number. Constrict: true, TypeScript se queja si algo puede serundefinedonully no lo compruebas. - Tipos que no escribes tú: Prisma genera los tipos a partir del schema.
findUniquedevuelveUser | null, y por eso hay que escribirfound?.name(optional chaining). async/await: todas las operaciones de Prisma devuelven promesas.- ES modules con extensión
.js: conNodeNext, los imports relativos llevan.jsaunque el archivo sea.ts→import { prisma } from './db.js';. Es el error número uno del primer día. - Ejecutar vs comprobar:
npx tsx src/archivo.tsejecuta (no comprueba tipos);npm run typecheckcomprueba tipos (no ejecuta).
5. schema.prisma: modelos y tipos
Sección titulada «5. schema.prisma: modelos y tipos»Estructura base del archivo (generada por prisma init):
generator client { provider = "prisma-client" output = "../generated/prisma"}
datasource db { provider = "postgresql"}Dos cambios respecto a lo que veréis en tutoriales antiguos (Prisma ≤ 6), y en mucho código que genera la IA:
- El generator es
prisma-clienty escribe el cliente engenerated/prisma(dentro de vuestro proyecto, ignorado en git), no ennode_modules. - El
datasourceya no llevaurl: la URL vive enprisma7.config.ts.
Primer modelo: User
Sección titulada «Primer modelo: User»✍️ A mano: los modelos User y Course se teclean (en prisma/schema.prisma, debajo del datasource); los enums se copian (📋 S1·13, más abajo).
model User { id Int @id @default(autoincrement()) email String @unique name String password String role Role @default(STUDENT) createdAt DateTime @default(now())
coursesTaught Course[]}Explicar campo a campo:
@id→ clave primaria.@default(autoincrement())→ equivalente aSERIALen Postgres.@unique→ constraint de unicidad a nivel de DB, no solo validación en código.role Role→ usa el enumRole(lo pegáis con 📋 S1·13): Prisma lo traduce a un tipoENUMnativo de Postgres.@default(now())→ timestamp automático en elINSERT.coursesTaught Course[]→ el lado “muchos” de la relación conCourse(ver abajo). No crea ninguna columna: solo le dice a Prisma que desde un usuario se puede navegar a sus cursos.
Segundo modelo con relación simple: Course
Sección titulada «Segundo modelo con relación simple: Course»model Course { id Int @id @default(autoincrement()) title String description String? price Decimal @db.Decimal(10, 2) level Level @default(BEGINNER) createdAt DateTime @default(now())
instructorId Int instructor User @relation(fields: [instructorId], references: [id])}Los enums: Role y Level
Sección titulada «Los enums: Role y Level»Se copian: añadidlos al final de prisma/schema.prisma.
// prisma/schema.prisma (añade al final, debajo de los modelos)enum Role { STUDENT INSTRUCTOR ADMIN}
enum Level { BEGINNER INTERMEDIATE ADVANCED}Una relación, dos lados
Sección titulada «Una relación, dos lados»La relación 1:N “un instructor imparte muchos cursos” se escribe en los dos modelos:
- En
Course:instructorId(la columna con la clave foránea) +instructor(el campo de relación que dice a qué apunta). - En
User:coursesTaught Course[], el lado “muchos”. No crea columna.
Prisma exige los dos lados: si falta uno, el schema no valida y la migración no arranca.
Micro-ejercicio 1 (3 min): rompe la relación
Sección titulada «Micro-ejercicio 1 (3 min): rompe la relación»Con User, Course y los enums ya en vuestro schema.prisma:
- Borrad la línea
coursesTaught Course[]deUser. - Ejecutad
npx prisma validate. ¿Qué dice? - Ejecutad
npx prisma formaty volved a mirarUser. ¿Qué ha pasado? - Dejadlo como estaba: el campo se llama
coursesTaught.
Referencia: tipos más comunes
Sección titulada «Referencia: tipos más comunes»| Prisma | PostgreSQL | Uso típico |
|---|---|---|
String |
TEXT / VARCHAR |
texto |
Int |
INTEGER |
ids, contadores |
Decimal |
DECIMAL |
dinero (nunca Float para precios) |
Boolean |
BOOLEAN |
flags |
DateTime |
TIMESTAMP |
fechas |
Json |
JSONB |
datos semi-estructurados |
Mención: dinero en Decimal, nunca en Float. Con Float un precio de 19.99 puede acabar como 19.990000000000002 por representación binaria. Por eso price es Decimal @db.Decimal(10, 2).
6. Migraciones y prisma generate
Sección titulada «6. Migraciones y prisma generate»6.1 Crear la primera migración
Sección titulada «6.1 Crear la primera migración»npx prisma migrate dev --name initnpx prisma generateQué hacen estos comandos (explicarlo paso a paso, es importante que no sea “magia”):
migrate devcompara elschema.prismacon el estado actual de la DB.- Genera un archivo SQL en
prisma/migrations/<timestamp>_init/migration.sql. - Ejecuta ese SQL contra la base de datos.
generatecrea el Prisma Client con los tipos de vuestro schema engenerated/prisma. En Prisma 7,migrate devya no lo hace: después de cada migración,npx prisma generate.
Prisma 7: dónde vive el cliente
Sección titulada «Prisma 7: dónde vive el cliente»El generator de schema.prisma (provider = "prisma-client", output = "../generated/prisma") hace que generate escriba el cliente en generated/prisma. Por eso, en Prisma 7:
// ✗ Prisma ≤ 6 (tutoriales antiguos y mucho código de IA)import { PrismaClient } from '@prisma/client';// ✓ Prisma 7 (vuestro proyecto)import { PrismaClient } from '../generated/prisma/client.js';Que package.json tenga @prisma/client no significa que se importe de ahí. Tenedlo delante en la revisión de §7.
Micro-ejercicio 2 (10 min): no es magia, es SQL
Sección titulada «Micro-ejercicio 2 (10 min): no es magia, es SQL»- Ejecutad los dos comandos de arriba.
- Abrid
prisma/migrations/<timestamp>_init/migration.sqly buscad:- ¿Dónde aparece
coursesTaught? - ¿En qué se ha convertido el
@uniquedeemail? - ¿Y el
@relationdeCourse.instructor?
- ¿Dónde aparece
- Commit (📋 S1·15):
git add . && git commit -m "Modelos User y Course"6.2 Menciones
Sección titulada «6.2 Menciones»- Prisma Studio (
npx prisma studio): UI enlocalhost:5555para ver y editar filas. Útil para depurar; opcional. migrate devvsmigrate deploy:deves para desarrollo local (interactivo, puede resetear la DB);deployes para CI/CD y producción (solo aplica migraciones pendientes, no pregunta). Hoy y en todo el bloque,dev;deployllega en el bloque de DevOps.
7. Prisma Client: CRUD revisando código generado por IA
Sección titulada «7. Prisma Client: CRUD revisando código generado por IA»Le hemos pedido a un asistente de IA: “Escríbeme un script con Prisma que haga create, read, update y delete de un usuario en mi proyecto AcademIA”. Esto es lo que devolvió. Copiadlo de la web y guardadlo como src/crud.ia.ts (lo ejecutaréis en §7.2, después de revisarlo):
// src/crud.ia.ts// ─────────────────────────────────────────────────────────────────────────// Generado por un asistente de IA · Sesión 1// Prompt: "Escríbeme un script con Prisma que haga create, read, update y// delete de un usuario en mi proyecto AcademIA"// Tu tarea: revísalo con la checklist ANTES de ejecutarlo. ¿Lo aceptarías?// ─────────────────────────────────────────────────────────────────────────import { PrismaClient } from '@prisma/client';
async function createUser(email: string, name: string) { const prisma = new PrismaClient(); return prisma.user.create({ data: { email, name, password: 'ana1234', role: 'INSTRUCTOR' }, });}
async function getUser(email: string) { const prisma = new PrismaClient(); return prisma.user.findFirst({ where: { email } });}
async function renameUser(id: number, name: string) { const prisma = new PrismaClient(); try { return await prisma.user.update({ where: { id }, data: { name } }); } catch (e) { console.log('Error'); }}
async function deleteUser(id: number) { const prisma = new PrismaClient(); await prisma.user.delete({ where: { id } });}
const user = await createUser('ana@academia.dev', 'Ana');console.log(await getUser(user.email));await renameUser(user.id, 'Ana García');await deleteUser(user.id);7.1 Revisión
Sección titulada «7.1 Revisión»¿Lo aceptaríais en una PR? Revisadlo antes de ejecutarlo con esta checklist:
- ¿La forma de importar y crear el cliente coincide con Prisma 7? (§6.1, “dónde vive el cliente”)
- ¿Cuántas instancias de
PrismaClientse crean? ¿Qué pasaría en una API con cientos de peticiones? -
emailes@unique: ¿es el método de búsqueda adecuado? - ¿Hay datos sensibles en claro?
- Si una operación falla, ¿se entera alguien?
- ¿Se cierra la conexión? ¿Tiene estructura de script?
7.2 Ejecutarlo
Sección titulada «7.2 Ejecutarlo»Con el fichero guardado como src/crud.ia.ts (📋 S1·16), ejecutadlo. ¿Qué pasa?
npx tsx src/crud.ia.ts7.3 Corregirlo
Sección titulada «7.3 Corregirlo»Primero, una sola instancia del cliente para todo el proyecto, con el adapter de Postgres (src/db.ts, se copia):
// src/db.tsimport 'dotenv/config';import { PrismaPg } from '@prisma/adapter-pg';import { PrismaClient } from '../generated/prisma/client.js';
// Una sola instancia para toda la aplicación (no una por request).const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL,});
export const prisma = new PrismaClient({ adapter });El cliente se importa del código generado (../generated/prisma/client.js), no de @prisma/client.
Ahora vuestro src/crud.ts. Copiad el esqueleto (el main, los console.log y el cierre) y ✍️ teclead a mano las llamadas de Prisma: create, findUnique por email, findMany con where, el findUnique de un id que no existe, update y delete.
// src/crud.ts · Ejecutar: npx tsx src/crud.ts// Esqueleto: escribe tú cada línea marcada ✍️ (el código completo está en las slides y justo debajo).import { prisma } from './db.js';
async function main() { // Create ✍️ const user = await prisma.user.create({ data: { … } }); // email `ana+${Date.now()}@academia.dev` (único en cada ejecución), name 'Ana', // password 'hashed-password-placeholder' (sesión 4: Argon2id), role 'INSTRUCTOR'
console.log('create →', user);
// Read ✍️ const found = … findUnique por email (user.email) // ✍️ const instructors = … findMany con where role 'INSTRUCTOR'
console.log('findUnique →', found?.name, '| instructores:', instructors.length);
// Si no existe → null (no excepción) ✍️ const maybeUser = … findUnique con id 999_999
console.log('id inexistente →', maybeUser);
// Update ✍️ const updated = … update por user.id, name 'Ana García'
console.log('update →', updated.name);
// Count console.log('total usuarios →', await prisma.user.count());
// Delete ✍️ … delete por user.id
console.log('delete → ok');}
main().finally(() => prisma.$disconnect());El fichero completo, con las líneas ✍️ escritas (es lo que veréis en las slides):
// src/crud.ts · Ejecutar: npx tsx src/crud.tsimport { prisma } from './db.js';
async function main() { // Create const user = await prisma.user.create({ data: { email: `ana+${Date.now()}@academia.dev`, // único en cada ejecución name: 'Ana', password: 'hashed-password-placeholder', // sesión 4: Argon2id role: 'INSTRUCTOR', }, }); console.log('create →', user);
// Read: uno por clave única, muchos con filtro const found = await prisma.user.findUnique({ where: { email: user.email } }); const instructors = await prisma.user.findMany({ where: { role: 'INSTRUCTOR' } }); console.log('findUnique →', found?.name, '| instructores:', instructors.length);
// Si no existe → null (no excepción) const maybeUser = await prisma.user.findUnique({ where: { id: 999_999 } }); console.log('id inexistente →', maybeUser);
// Update const updated = await prisma.user.update({ where: { id: user.id }, data: { name: 'Ana García' }, }); console.log('update →', updated.name);
// Count console.log('total usuarios →', await prisma.user.count());
// Delete await prisma.user.delete({ where: { id: user.id } }); console.log('delete → ok');}
main().finally(() => prisma.$disconnect());Las operaciones, en una línea cada una:
create: inserta una fila y devuelve el objeto creado (con suid). Si elemailya existe, falla (constraint@unique): por eso elDate.now()en el email, para poder ejecutar el script varias veces.findUnique: busca por un campo@ido@unique. Devuelve el objeto onullsi no existe, nunca lanza.findMany: lista con filtro (where). Devuelve un array, vacío si no hay coincidencias.update: modifica por clave única. Si no existe la fila, lanza error (no devuelvenull).count: cuenta filas, con o sinwhere.delete: borra por clave única; también lanza si no existe.main().finally(() => prisma.$disconnect()): la plantilla de todos nuestros scripts. Envuelve el código en una función y cierra la conexión pase lo que pase, para que el proceso termine.
npx tsx src/crud.tsrm src/crud.ia.ts # no se commitea: rompería el typecheckgit add . && git commit -m "Cliente Prisma y CRUD"8. Práctica: extender el schema + seed idempotente
Sección titulada «8. Práctica: extender el schema + seed idempotente»¿Te atascaste en el setup? Engánchate aquí desde el punto de control origin/s1-checkpoint (setup + User y Course + migración init + db.ts + crud.ts, aún sin seed) con la receta única “Cambiar de rama”, la misma en todas las sesiones. Hoy la tercera línea ya apunta al checkpoint:
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s1-cp origin/s1-checkpointnpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seedTraer un solo fichero: git checkout origin/s1-checkpoint -- ruta/del/fichero.
- Hoy el reset es esperado y aún no hay
npm run seed(lo escribís en la práctica):migrate devos pedirá reset porque vuestras migraciones tienen otro timestamp que las de la rama, ys1-checkpointtodavía no tiene seed, así que hoy elnpm run seeddel comentario no aplica. - Windows: todo en Git Bash (no PowerShell).
- En un clon, las ramas del curso solo existen como
origin/…: por eso elfetchy elorigin/delante. - Con el zip del campus: descomprimid el zip de
s1-checkpointen una carpeta nueva, copiad vuestro.envy seguid desdenpm install. - Los scripts
db:*delpackage.jsonde la rama (db:up,db:migrate,db:generate,db:studio) son atajos opcionales: no los usamos en clase. - La forma genérica de la receta (
git switch -c mi-sN origin/sN-start, omi-sN-cp origin/sN-checkpoint) está en “Si te quedas atrás”, al final.
Enunciado para el alumno:
Vais a extender el schema de academia-api con lo que reutilizarán las próximas sesiones. Con lo visto hoy:
- Añadir al modelo
Courseun campopublishedbooleano,falsepor defecto. - Crear un modelo
Category(id,nameúnico) — sin relacionarlo todavía conCourse: lo haremos el miércoles (sesión 2), cuando una categoría tenga muchos cursos (1:N). - Generar la migración y el cliente (📋 S1·22, debajo de esta lista).
- Escribir un script
src/seed-manual.tsque:- Cree la instructora
marta@academia.dev(«Marta», rolINSTRUCTOR) y las categoríasBackendyBases de datos(emailynameson@unique→upsert). - Cree 2 cursos de Marta: «TypeScript desde cero» (publicado,
BEGINNER) y «Prisma avanzado» (sin publicar,ADVANCED) (deleteMany+createMany, ver abajo). Usa estos nombres: el seed de la sesión 2 parte de estos datos. - Se pueda ejecutar dos veces seguidas sin fallar ni duplicar datos.
- Cree la instructora
- Crear el script
seedenpackage.json(se queda para todo el bloque: las próximas sesiones lo dan por hecho), ejecutarlo dos veces y hacer commit (📋 S1·23, debajo).
Paso 3, migración y cliente:
npx prisma migrate dev --name add_category_and_publishednpx prisma generatePaso 5, script seed y doble ejecución:
npm pkg set scripts.seed="tsx src/seed-manual.ts" # una veznpm run seed && npm run seedTres métodos para el seed:
// upsert: where por un campo @id o @unique; si existe → update, si no → createawait prisma.category.upsert({ where: { name: 'Backend' }, update: {}, // vacío: si ya existe, no toca nada create: { name: 'Backend' },});// deleteMany: borra todas las filas que cumplan el whereawait prisma.course.deleteMany({ where: { instructorId: 1 } });// createMany: inserta un array de filas de una vezawait prisma.course.createMany({ data: [{ /* curso 1 */ }, { /* curso 2 */ }] });⚠ Course.title no es @unique, así que course.upsert por título no funciona (PrismaClientValidationError, pide id). Borrad los cursos del instructor (deleteMany) y volved a crearlos (createMany).
Criterio de “hecho”:
npm run seedse ejecuta dos veces seguidas sin error ni duplicados.- Commit hecho (
git statuslimpio, sin.env).
Opcional, si terminas antes:
- Al final del seed, un
findManyque imprima solo los cursos publicados; después, solo los publicados de nivelBEGINNER(dos condiciones en el mismowhere). - Mirar en
prisma/migrations/el SQL de la segunda migración: ¿quéALTER TABLEha generadopublished? - Abrir Prisma Studio y comprobar que no hay usuarios, cursos ni categorías duplicados.
9. Cierre, cómo recuperarse y adelanto de la sesión 2
Sección titulada «9. Cierre, cómo recuperarse y adelanto de la sesión 2»Resumen de lo aprendido hoy (pedir que lo digan ellos, no darlo tú):
- Qué es un ORM y cuándo NO usarlo.
- Prisma 7: schema →
migrate dev→generate→ cliente. - Una relación 1:N se escribe por los dos lados.
- CRUD básico con Prisma Client, y por qué hay que revisar lo que genera la IA.
Si te quedas atrás (vale para todo el bloque)
Sección titulada «Si te quedas atrás (vale para todo el bloque)»El repo de referencia tiene, por sesión, sN-start (punto de partida), sN-checkpoint (lo hecho en clase antes de la práctica) y sN-end (solución). Para saltar a cualquiera de ellas, la receta única “Cambiar de rama” (el miércoles, con origin/s2-start; desde s1-end ya existe npm run seed):
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)git fetch origingit switch -c mi-s2 origin/s2-start # o: git switch -c mi-s2-cp origin/s2-checkpointnpm installnpx prisma generatenpx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seedTraer un solo fichero: git checkout origin/s2-checkpoint -- ruta/del/fichero.
- Windows: todo en Git Bash (no PowerShell).
- En un clon, las ramas del curso solo existen como
origin/…:git checkout s2-starta secas falla. - Tu
.envno está en git, así que se conserva al cambiar de rama. Si no lo tienes:cp .env.example .env(con el plan B, pon tu URLpostgres://denpx prisma dev --detach). - Las carpetas de migración de la rama llevan el timestamp de quien las generó, no el tuyo: por eso
migrate devpide reset. Es esperado; solo se pierden datos de prueba. - Con el zip del campus en vez de git: descomprime el zip de la rama en una carpeta nueva, copia tu
.envy sigue desdenpm install.
Adelanto sesión 2 (miércoles 25 nov): relaciones 1:N (Category → Course) y N:M “de verdad” (la tabla Enrollment que conecta User y Course), include/select, paginación, transacciones y el problema N+1 en detalle.