Inicio / TypeScript / Node.js Backend con TypeScript / Prisma ORM: CRUD y migraciones

Prisma ORM: CRUD y migraciones

Modela tu base de datos con Prisma Schema, migraciones y operaciones CRUD.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Base de datos con Prisma ORM: setup y CRUD

¿Qué es Prisma?

Prisma es un ORM de siguiente generación para Node.js y TypeScript. A diferencia de ORMs clásicos (Sequelize, TypeORM), Prisma genera un cliente completamente tipado a partir de tu schema — no necesitas declarar tipos manualmente.

Componentes:

  • Prisma Schema (prisma/schema.prisma) — define modelos y relaciones
  • Prisma Migrate — genera y aplica migraciones SQL
  • Prisma Client — cliente generado con tipos perfectos para tu schema
  • Prisma Studio — GUI visual para explorar la BD

Instalación y setup

pnpm add @prisma/client
pnpm add -D prisma

# Inicializar (crea prisma/schema.prisma y .env)
npx prisma init --datasource-provider postgresql

Schema de Prisma

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(VIEWER)
  active    Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relaciones (se definen en la lección siguiente)
  posts    Post[]
  profile  Profile?

  @@index([email])
  @@map("users") // nombre de la tabla en la BD
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  avatar String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("profiles")
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  content     String    @db.Text
  published   Boolean   @default(false)
  publishedAt DateTime?
  viewCount   Int       @default(0)
  authorId    Int
  author      User      @relation(fields: [authorId], references: [id])
  tags        Tag[]     @relation("PostTags")
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  @@index([authorId])
  @@index([published, publishedAt(sort: Desc)])
  @@map("posts")
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  slug  String @unique
  posts Post[] @relation("PostTags")

  @@map("tags")
}

enum Role {
  ADMIN
  EDITOR
  VIEWER
}

Migraciones

# Crear y aplicar migración en desarrollo
npx prisma migrate dev --name "create-users-posts-tags"
# Esto: genera el SQL, lo aplica, y regenera el Prisma Client

# Aplicar migraciones en producción (sin generar nuevas)
npx prisma migrate deploy

# Ver estado de migraciones
npx prisma migrate status

# Regenerar cliente sin migrar (después de cambiar el schema manualmente)
npx prisma generate

# Reset completo de la BD (solo en desarrollo)
npx prisma migrate reset

# Explorar la BD visualmente
npx prisma studio

Inicializar el cliente

// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
import { config } from '../config/index.js';

// Singleton: evitar múltiples instancias en desarrollo con hot-reload
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: config.app.isDev
      ? ['query', 'info', 'warn', 'error']
      : ['warn', 'error'],
  });

if (config.app.isDev) {
  globalForPrisma.prisma = prisma;
}

// Desconectar correctamente al cerrar la app
process.on('beforeExit', async () => {
  await prisma.$disconnect();
});

CRUD completo

CREATE

import { prisma } from '../lib/prisma.js';
import { Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';

// Crear un registro
async function createUser(data: {
  name: string;
  email: string;
  password: string;
}) {
  const hashedPassword = await bcrypt.hash(data.password, 12);

  const user = await prisma.user.create({
    data: {
      name:     data.name,
      email:    data.email,
      password: hashedPassword,
      // Crear relación anidada en la misma operación
      profile: {
        create: { bio: '' },
      },
    },
    // Seleccionar qué campos retornar
    select: {
      id:        true,
      name:      true,
      email:     true,
      role:      true,
      createdAt: true,
      // password: false — omitir campos sensibles
    },
  });

  return user;
}

// Crear múltiples registros eficientemente
async function createManyTags(tags: { name: string; slug: string }[]) {
  return prisma.tag.createMany({
    data: tags,
    skipDuplicates: true, // ignorar duplicados en vez de fallar
  });
}

READ

// Encontrar por ID (null si no existe)
async function findUserById(id: number) {
  return prisma.user.findUnique({
    where: { id },
  });
}

// Lanzar error si no existe (útil en controladores)
async function getUserOrThrow(id: number) {
  return prisma.user.findUniqueOrThrow({
    where: { id },
    select: { id: true, name: true, email: true, role: true },
  });
}

// Encontrar primero que coincida con criterios
async function findByEmail(email: string) {
  return prisma.user.findFirst({
    where: { email, active: true },
  });
}

// Listar con paginación
async function getUsers(page = 1, pageSize = 20) {
  const skip = (page - 1) * pageSize;

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      where:   { active: true },
      select:  { id: true, name: true, email: true, role: true, createdAt: true },
      orderBy: { createdAt: 'desc' },
      skip,
      take:    pageSize,
    }),
    prisma.user.count({ where: { active: true } }),
  ]);

  return {
    data:  users,
    total,
    page,
    pages: Math.ceil(total / pageSize),
  };
}

UPDATE

// Actualizar por clave única
async function updateUser(id: number, data: Prisma.UserUpdateInput) {
  return prisma.user.update({
    where: { id },
    data,
    select: { id: true, name: true, email: true, role: true, updatedAt: true },
  });
}

// updateMany — actualizar varios registros a la vez
async function deactivateInactiveUsers(beforeDate: Date) {
  const result = await prisma.user.updateMany({
    where: {
      active:    true,
      updatedAt: { lt: beforeDate },
    },
    data: { active: false },
  });

  return result.count; // número de registros actualizados
}

// upsert — crear si no existe, actualizar si existe
async function upsertUser(email: string, name: string) {
  return prisma.user.upsert({
    where:  { email },
    update: { name },
    create: { email, name, password: '' },
  });
}

DELETE

// Eliminar por clave única (lanza error si no existe)
async function deleteUser(id: number) {
  return prisma.user.delete({ where: { id } });
}

// Soft delete (marcar como inactivo en vez de eliminar)
async function softDeleteUser(id: number) {
  return prisma.user.update({
    where: { id },
    data:  { active: false },
  });
}

// Eliminar múltiples
async function deleteDraftPosts(authorId: number) {
  const result = await prisma.post.deleteMany({
    where: { authorId, published: false },
  });
  return result.count;
}

Transacciones

Cuando necesitas que múltiples operaciones sean atómicas (todo o nada):

// Transacción interactiva — más flexible, permite lógica condicional
async function transferCredits(fromId: number, toId: number, amount: number) {
  return prisma.$transaction(async (tx) => {
    // 1. Verificar saldo suficiente
    const sender = await tx.user.findUniqueOrThrow({ where: { id: fromId } });
    // (asumiendo que User tiene un campo credits)

    // 2. Descontar del remitente
    await tx.user.update({
      where: { id: fromId },
      data:  { credits: { decrement: amount } },
    });

    // 3. Acreditar al destinatario
    await tx.user.update({
      where: { id: toId },
      data:  { credits: { increment: amount } },
    });

    // Si cualquier paso falla → rollback automático
  });
}

// Transacción batch — más rápida, sin lógica condicional
async function createPostWithTags(postData: { title: string; authorId: number }, tagIds: number[]) {
  return prisma.$transaction([
    prisma.post.create({
      data: {
        ...postData,
        content: '',
        tags: { connect: tagIds.map(id => ({ id })) },
      },
    }),
    prisma.user.update({
      where: { id: postData.authorId },
      data:  { postCount: { increment: 1 } },
    }),
  ]);
}

Manejo de errores de Prisma

import { Prisma } from '@prisma/client';

async function safeCreateUser(data: { email: string; name: string; password: string }) {
  try {
    return await createUser(data);
  } catch (err) {
    if (err instanceof Prisma.PrismaClientKnownRequestError) {
      // P2002: violación de constraint único
      if (err.code === 'P2002') {
        const field = (err.meta?.target as string[])?.join(', ');
        throw new AppError(409, `El ${field} ya está en uso`);
      }
      // P2025: registro no encontrado (update/delete)
      if (err.code === 'P2025') {
        throw new AppError(404, 'Registro no encontrado');
      }
    }
    throw err; // re-lanzar errores desconocidos
  }
}

Resumen

Operación Método Prisma
Crear uno prisma.model.create({ data })
Crear muchos prisma.model.createMany({ data })
Buscar por ID único prisma.model.findUnique({ where })
Buscar primero prisma.model.findFirst({ where })
Listar prisma.model.findMany({ where, skip, take, orderBy })
Contar prisma.model.count({ where })
Actualizar prisma.model.update({ where, data })
Crear o actualizar prisma.model.upsert({ where, create, update })
Eliminar prisma.model.delete({ where })
Transacción prisma.$transaction([...]) o prisma.$transaction(async tx => ...)

En la siguiente lección profundizamos en relaciones y consultas avanzadas: include, select anidado, filtros complejos, agregaciones y queries con $queryRaw.

🔒

Ejercicio práctico disponible

Implementa un repositorio CRUD genérico en memoria

Desbloquear ejercicios
// Implementa un repositorio CRUD genérico en memoria
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes