Ir al contenido

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


Al terminar, el alumno debe ser capaz de:

  1. Explicar qué es un ORM y justificar cuándo NO usarlo.
  2. Leer y escribir el TypeScript mínimo del bloque: tipos, async/await e imports con .js.
  3. Definir modelos en schema.prisma: campos, tipos, @id, @unique y enums.
  4. Modelar una relación 1:N con sus dos lados.
  5. Aplicar cambios de schema con migrate dev + generate y leer el SQL generado.
  6. 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.


  • 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 &&, # y rm, que no funcionan en cmd ni en Windows PowerShell 5.1
  • Acceso al repo academia-api (GitHub) o el zip de la rama s1-start descargado 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 mano
SELECT * FROM "Course" WHERE "published" = true;
// Con ORM: Prisma
const 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:

📋 S1·01 · clonar el repo y crear tu rama (terminal)
git clone -b s1-start \
https://github.com/EmilioCarrion/academia-api-alumnos.git academia-api
cd academia-api
git switch -c mi-trabajo

Sin 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.


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.

  • 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).
  • 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 pg client 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.


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):

📋 S1·02 · proyecto Node + dependencias (terminal)
npm init -y
npm pkg set type=module
npm i -D prisma@7 typescript tsx @types/node
npm i @prisma/client@7 @prisma/adapter-pg pg dotenv

"type": "module" es obligatorio: Prisma 7 genera el cliente como ES modules.

  • prisma (CLI) y @prisma/client fijados a la 7. Sin el @7, hoy npm instala la 8, que está en release candidate.
  • typescript + @types/node: tipos. tsx: ejecuta .ts directamente, 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 driver pg), es obligatorio.
  • npm mostrará avisos de allow-scripts: no son errores, seguid.
  • dotenv: Prisma 7 ya no carga el .env solo.

Crear docker-compose.yml en la raíz del proyecto:

📋 S1·03 · docker-compose.yml completo
# 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:

📋 S1·04 · levantar PostgreSQL con Docker (terminal)
docker compose up -d
docker compose ps
# esperad a ver (healthy) en STATUS

El 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.

Prisma 7 trae un Postgres local que no necesita Docker:

📋 S1·05 · plan B, Postgres de Prisma sin Docker (terminal)
npx prisma dev --name academia --detach

La 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=disable

Si 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).

📋 S1·06 · inicializar Prisma (terminal)
npx prisma init --datasource-provider postgresql

Esto genera:

prisma/
schema.prisma ← modelos
prisma7.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:

📋 S1·07 · borrar las skills que deja prisma init (terminal)
rm -rf .claude .agents .windsurf skills-lock.json

Si 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):

📋 S1·08 · .env y .env.example
# .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"],
},
});

Comprobad que el .gitignore generado contiene al menos estas líneas (si falta alguna, añadidla):

📋 S1·09 · líneas mínimas del .gitignore
# .gitignore (al menos estas líneas)
node_modules
.env
/generated/prisma

📋 S1·10 · comprobar y hacer el primer commit (terminal)
cat .gitignore
git add .
git status # ni .env ni .claude/ deben aparecer
git commit -m "Setup inicial: Postgres + Prisma 7"

Lo justo para leer y escribir el código de este bloque. Configuración (tsconfig.json en la raíz):

📋 S1·11 · tsconfig.json completo
// 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):

📋 S1·12 · script typecheck en package.json (terminal)
npm pkg set scripts.typecheck="tsc --noEmit"

En package.json queda "typecheck": "tsc --noEmit" dentro de "scripts".

Cinco ideas que vais a ver hoy:

  1. Anotaciones de tipo: name: string, id: number. Con strict: true, TypeScript se queja si algo puede ser undefined o null y no lo compruebas.
  2. Tipos que no escribes tú: Prisma genera los tipos a partir del schema. findUnique devuelve User | null, y por eso hay que escribir found?.name (optional chaining).
  3. async/await: todas las operaciones de Prisma devuelven promesas.
  4. ES modules con extensión .js: con NodeNext, los imports relativos llevan .js aunque el archivo sea .ts → import { prisma } from './db.js';. Es el error número uno del primer día.
  5. Ejecutar vs comprobar: npx tsx src/archivo.ts ejecuta (no comprueba tipos); npm run typecheck comprueba tipos (no ejecuta).

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-client y escribe el cliente en generated/prisma (dentro de vuestro proyecto, ignorado en git), no en node_modules.
  • El datasource ya no lleva url: la URL vive en prisma7.config.ts.

✍️ 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 a SERIAL en Postgres.
  • @unique → constraint de unicidad a nivel de DB, no solo validación en código.
  • role Role → usa el enum Role (lo pegáis con 📋 S1·13): Prisma lo traduce a un tipo ENUM nativo de Postgres.
  • @default(now()) → timestamp automático en el INSERT.
  • coursesTaught Course[] → el lado “muchos” de la relación con Course (ver abajo). No crea ninguna columna: solo le dice a Prisma que desde un usuario se puede navegar a sus cursos.
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])
}

Se copian: añadidlos al final de prisma/schema.prisma.

📋 S1·13 · enums Role y Level (prisma/schema.prisma)
// prisma/schema.prisma (añade al final, debajo de los modelos)
enum Role {
STUDENT
INSTRUCTOR
ADMIN
}
enum Level {
BEGINNER
INTERMEDIATE
ADVANCED
}

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:

  1. Borrad la línea coursesTaught Course[] de User.
  2. Ejecutad npx prisma validate. ¿Qué dice?
  3. Ejecutad npx prisma format y volved a mirar User. ¿Qué ha pasado?
  4. Dejadlo como estaba: el campo se llama coursesTaught.
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).


📋 S1·14 · primera migración y cliente (terminal)
npx prisma migrate dev --name init
npx prisma generate

Qué hacen estos comandos (explicarlo paso a paso, es importante que no sea “magia”):

  1. migrate dev compara el schema.prisma con el estado actual de la DB.
  2. Genera un archivo SQL en prisma/migrations/<timestamp>_init/migration.sql.
  3. Ejecuta ese SQL contra la base de datos.
  4. generate crea el Prisma Client con los tipos de vuestro schema en generated/prisma. En Prisma 7, migrate dev ya no lo hace: después de cada migración, npx prisma generate.

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»
  1. Ejecutad los dos comandos de arriba.
  2. Abrid prisma/migrations/<timestamp>_init/migration.sql y buscad:
    • ¿Dónde aparece coursesTaught?
    • ¿En qué se ha convertido el @unique de email?
    • ¿Y el @relation de Course.instructor?
  3. Commit (📋 S1·15):

📋 S1·15 · commit de los modelos (terminal)
git add . && git commit -m "Modelos User y Course"
  • Prisma Studio (npx prisma studio): UI en localhost:5555 para ver y editar filas. Útil para depurar; opcional.
  • migrate dev vs migrate deploy: dev es para desarrollo local (interactivo, puede resetear la DB); deploy es para CI/CD y producción (solo aplica migraciones pendientes, no pregunta). Hoy y en todo el bloque, dev; deploy llega 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):

📋 S1·16 · código de la IA para revisar (src/crud.ia.ts)
// 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);

¿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 PrismaClient se crean? ¿Qué pasaría en una API con cientos de peticiones?
  • email es @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?

Con el fichero guardado como src/crud.ia.ts (📋 S1·16), ejecutadlo. ¿Qué pasa?

📋 S1·17 · ejecutar el código de la IA (terminal)
npx tsx src/crud.ia.ts

Primero, una sola instancia del cliente para todo el proyecto, con el adapter de Postgres (src/db.ts, se copia):

📋 S1·18 · src/db.ts completo
// src/db.ts
import '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.

📋 S1·19 · esqueleto de src/crud.ts (las llamadas ✍️ las escribes tú)
// 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.ts
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: 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 su id). Si el email ya existe, falla (constraint @unique): por eso el Date.now() en el email, para poder ejecutar el script varias veces.
  • findUnique: busca por un campo @id o @unique. Devuelve el objeto o null si 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 devuelve null).
  • count: cuenta filas, con o sin where.
  • 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.

📋 S1·20 · ejecutar el CRUD, limpiar y commit (terminal)
npx tsx src/crud.ts
rm src/crud.ia.ts # no se commitea: rompería el typecheck
git 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:

📋 S1·21 · receta «Cambiar de rama» al checkpoint (terminal, rescate)
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s1-cp origin/s1-checkpoint
npm install
npx prisma generate
npx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed

Traer 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 dev os pedirá reset porque vuestras migraciones tienen otro timestamp que las de la rama, y s1-checkpoint todavía no tiene seed, así que hoy el npm run seed del comentario no aplica.
  • Windows: todo en Git Bash (no PowerShell).
  • En un clon, las ramas del curso solo existen como origin/…: por eso el fetch y el origin/ delante.
  • Con el zip del campus: descomprimid el zip de s1-checkpoint en una carpeta nueva, copiad vuestro .env y seguid desde npm install.
  • Los scripts db:* del package.json de 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, o mi-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:

  1. Añadir al modelo Course un campo published booleano, false por defecto.
  2. Crear un modelo Category (id, name único) — sin relacionarlo todavía con Course: lo haremos el miércoles (sesión 2), cuando una categoría tenga muchos cursos (1:N).
  3. Generar la migración y el cliente (📋 S1·22, debajo de esta lista).
  4. Escribir un script src/seed-manual.ts que:
    • Cree la instructora marta@academia.dev («Marta», rol INSTRUCTOR) y las categorías Backend y Bases de datos (email y name son @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.
  5. Crear el script seed en package.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:

📋 S1·22 · migración de la práctica y cliente (terminal)
npx prisma migrate dev --name add_category_and_published
npx prisma generate

Paso 5, script seed y doble ejecución:

📋 S1·23 · script seed y ejecutarlo dos veces (terminal)
npm pkg set scripts.seed="tsx src/seed-manual.ts" # una vez
npm run seed && npm run seed

Tres métodos para el seed:

// upsert: where por un campo @id o @unique; si existe → update, si no → create
await 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 where
await prisma.course.deleteMany({ where: { instructorId: 1 } });
// createMany: inserta un array de filas de una vez
await 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 seed se ejecuta dos veces seguidas sin error ni duplicados.
  • Commit hecho (git status limpio, sin .env).

Opcional, si terminas antes:

  • Al final del seed, un findMany que imprima solo los cursos publicados; después, solo los publicados de nivel BEGINNER (dos condiciones en el mismo where).
  • Mirar en prisma/migrations/ el SQL de la segunda migración: ¿qué ALTER TABLE ha generado published?
  • 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):

📋 S1·25 · receta «Cambiar de rama» para la sesión 2 (terminal, rescate)
git add -A && git commit -m "wip: mi trabajo" # guarda lo tuyo (si hay algo)
git fetch origin
git switch -c mi-s2 origin/s2-start # o: git switch -c mi-s2-cp origin/s2-checkpoint
npm install
npx prisma generate
npx prisma migrate dev # desde tu rama pedirá reset (esperado): npx prisma migrate reset y npm run seed

Traer 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-start a secas falla.
  • Tu .env no 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 URL postgres:// de npx prisma dev --detach).
  • Las carpetas de migración de la rama llevan el timestamp de quien las generó, no el tuyo: por eso migrate dev pide 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 .env y sigue desde npm 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.