Getting Started (Actualizado: 3/6/2026)

Prisma ORM con Claude Code: schema, migraciones, transacciones y tests

Implementa Prisma ORM con Claude Code: diseño de schema, revisión de migraciones, transactions, seed/test y checklist.

Prisma ORM con Claude Code: schema, migraciones, transacciones y tests

Prisma ORM es una capa de base de datos tipada para trabajar con una DB de forma segura desde TypeScript. Con Claude Code, no solo sirve para generar CRUD: puedes pedir diseño de schema, revisión de migration SQL, uso de Prisma Client, límites de transaction, seed, tests y checklist de publicación.

El problema aparece cuando se automatiza sin criterio. Una petición vaga como “hazme el CRUD con Prisma” puede dejar índices incompletos,include demasiado grandes, reglas cascade peligrosas o migraciones que pasan en local pero fallan con datos reales. Este artículo usa una API de blog para mostrar un flujo práctico, listo para un sitio de contenido, SaaS o herramienta interna.

Consulta las referencias oficiales: Prisma ORM, Prisma Schema, Transactions y Prisma Migrate. También ayudan las guías internas de Claude Code, Drizzle ORM, Supabase y Redis caching.

Flujo recomendado

flowchart LR
  A["Reglas del producto para Claude Code"] --> B["Diseñar schema.prisma"]
  B --> C["Generar y revisar migration.sql"]
  C --> D["Implementar queries con Prisma Client"]
  D --> E["Agregar seed y tests"]
  E --> F["Checklist antes de producción"]

Piensa en tres artefactos: schema, SQL migration y código TypeScript. Claude Code puede preparar los tres, pero una persona debe validar constraints, índices, reglas de borrado y fronteras de transaction.

Prompt útil para Claude Code

Diseña una capa Prisma ORM para una API de blog.

Contexto:
- TypeScript + Prisma ORM + SQLite en local, con posible migración futura a PostgreSQL
- Modelos: User, Post, Category, Comment, Notification, AuditLog
- Post usa status como string: DRAFT/PUBLISHED/ARCHIVED
- email y slug deben ser únicos
- La lista pública filtra por status, publishedAt, author y category
- Al borrar un Post se eliminan en cascade Comment y filas de unión
- Borrar User queda restringido; la anonimización será otra migration

Entrega:
1. prisma/schema.prisma
2. Puntos de revisión para migration SQL
3. Código Prisma Client para create/list/publish
4. Comandos de seed y test
5. Checklist de producción

La clave es incluir intención operativa: qué datos son únicos, qué no debe borrarse, qué pantallas necesitan índices y qué acciones requieren atomicidad.

Setup local

{
  "type": "module",
  "scripts": {
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:deploy": "prisma migrate deploy",
    "db:seed": "prisma db seed",
    "dev": "tsx src/demo.ts",
    "test": "vitest run"
  },
  "dependencies": {
    "@prisma/client": "latest",
    "dotenv": "latest"
  },
  "devDependencies": {
    "prisma": "latest",
    "tsx": "latest",
    "typescript": "latest",
    "vitest": "latest"
  }
}
npm install
echo 'DATABASE_URL="file:./dev.db"' > .env
mkdir prisma src
// prisma.config.ts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts",
  },
  datasource: {
    url: env("DATABASE_URL"),
  },
});

schema.prisma

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "sqlite"
}

model User {
  id            String         @id @default(cuid())
  email         String         @unique
  name          String
  role          String         @default("editor")
  posts         Post[]
  comments      Comment[]
  notifications Notification[]
  createdAt     DateTime       @default(now())
  updatedAt     DateTime       @updatedAt

  @@index([role])
}

model Post {
  id          String              @id @default(cuid())
  slug        String              @unique
  title       String
  body        String
  status      String              @default("DRAFT")
  authorId    String
  author      User                @relation(fields: [authorId], references: [id], onDelete: Restrict)
  categories  CategoriesOnPosts[]
  comments    Comment[]
  publishedAt DateTime?
  createdAt   DateTime            @default(now())
  updatedAt   DateTime            @updatedAt

  @@index([authorId])
  @@index([status, publishedAt])
}

model Category {
  id    String              @id @default(cuid())
  slug  String              @unique
  name  String
  posts CategoriesOnPosts[]
}

model CategoriesOnPosts {
  postId     String
  categoryId String
  assignedAt DateTime @default(now())
  post       Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  category   Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)

  @@id([postId, categoryId])
}

model Comment {
  id        String   @id @default(cuid())
  body      String
  postId    String
  authorId  String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  author    User     @relation(fields: [authorId], references: [id], onDelete: Restrict)
  createdAt DateTime @default(now())

  @@index([postId, createdAt])
  @@index([authorId])
}

model Notification {
  id        String    @id @default(cuid())
  userId    String
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  type      String
  message   String
  readAt    DateTime?
  createdAt DateTime  @default(now())

  @@index([userId, readAt])
}

model AuditLog {
  id        String   @id @default(cuid())
  action    String
  targetId  String
  metadata  String?
  createdAt DateTime @default(now())

  @@index([action, createdAt])
}

Pide a Claude Code que justifique cada índice. status + publishedAt sirve a la lista pública; postId + createdAt sirve a los comentarios. Si un índice no tiene pantalla o consulta asociada, probablemente sobra.

Revisión de migraciones

npx prisma format
npx prisma migrate dev --create-only --name init_blog
# Revisar prisma/migrations/*/migration.sql en el PR
npx prisma migrate dev
npx prisma generate

En producción:

npx prisma migrate deploy

db push es cómodo para prototipos, pero no sustituye una historia de migrations revisable. Antes de publicar, revisaDROP, nuevosNOT NULL, unique constraints sobre datos existentes y cambios en cascade.

La guía de rollback debe estar en la PR antes del deploy. Si la migration ya terminó bien, normalmente conviene crear otra migration hacia adelante que revierta el cambio. Si falló a medias, sigue el workflow oficial de down migrations: verificar backup, ejecutar migrate status, aplicar SQL revisado si hace falta y marcar solo esa migration como rolled back con migrate resolve.

npx prisma migrate status
npx prisma migrate resolve --rolled-back "20260603090000_failed_change"

Prisma Client y transactions

// src/db.ts
import { PrismaClient } from "./generated/prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: ["warn", "error"],
  });

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}
// src/posts.ts
import { Prisma } from "./generated/prisma/client";
import { prisma } from "./db";

export async function listPublishedPosts(params: {
  page?: number;
  perPage?: number;
  categorySlug?: string;
  search?: string;
}) {
  const page = Math.max(params.page ?? 1, 1);
  const perPage = Math.min(Math.max(params.perPage ?? 20, 1), 50);

  const where: Prisma.PostWhereInput = {
    status: "PUBLISHED",
    ...(params.categorySlug
      ? { categories: { some: { category: { slug: params.categorySlug } } } }
      : {}),
    ...(params.search
      ? { OR: [{ title: { contains: params.search } }, { body: { contains: params.search } }] }
      : {}),
  };

  const [items, total] = await prisma.$transaction([
    prisma.post.findMany({
      where,
      skip: (page - 1) * perPage,
      take: perPage,
      orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
      select: {
        id: true,
        slug: true,
        title: true,
        publishedAt: true,
        author: { select: { name: true } },
        _count: { select: { comments: true } },
      },
    }),
    prisma.post.count({ where }),
  ]);

  return { items, pagination: { page, perPage, total } };
}

export async function publishPost(postId: string) {
  return prisma.$transaction(async (tx) => {
    const post = await tx.post.findUnique({
      where: { id: postId },
      select: { id: true, title: true, status: true, authorId: true },
    });

    if (!post) throw new Error("Post not found");
    if (post.status === "PUBLISHED") return post;

    const published = await tx.post.update({
      where: { id: post.id },
      data: { status: "PUBLISHED", publishedAt: new Date() },
      select: { id: true, title: true, status: true, publishedAt: true },
    });

    await tx.notification.create({
      data: {
        userId: post.authorId,
        type: "POST_PUBLISHED",
        message: `${post.title} was published`,
      },
    });

    await tx.auditLog.create({
      data: {
        action: "POST_PUBLISHED",
        targetId: post.id,
        metadata: JSON.stringify({ title: post.title }),
      },
    });

    return published;
  });
}

Usa$transaction([]) para lecturas independientes como lista y count. Usa una transaction interactiva cuando una operación de negocio debe tocar varias tablas de forma atómica. No envíes emails ni llames APIs externas dentro de la transaction.

SQL seguro

Primero usa Prisma Client. Cuando una consulta de reporting necesite SQL, el pitfall es concatenar strings. La documentación de raw queries de Prisma recomienda tagged templates o Prisma.sql para parametrizar valores.

import { Prisma } from "./generated/prisma/client";
import { prisma } from "./db";

export async function topAuthors(limit = 10) {
  return prisma.$queryRaw<
    { authorId: string; postCount: bigint }[]
  >(Prisma.sql`
    SELECT "authorId", COUNT(*) AS "postCount"
    FROM "Post"
    WHERE "status" = ${"PUBLISHED"}
    GROUP BY "authorId"
    ORDER BY "postCount" DESC
    LIMIT ${Math.min(limit, 50)}
  `);
}

Pide a Claude Code que justifique cada $queryRawUnsafe. Si solo quiere cambiar nombres de tabla o columna dinámicamente, usa una allowlist revisada por una persona.

Seed, test y checklist

// prisma/seed.ts
import { PrismaClient } from "../src/generated/prisma/client";

const prisma = new PrismaClient();

async function main() {
  const user = await prisma.user.upsert({
    where: { email: "editor@example.com" },
    update: { name: "Editor" },
    create: { email: "editor@example.com", name: "Editor", role: "admin" },
  });

  await prisma.post.upsert({
    where: { slug: "claude-code-prisma-demo" },
    update: { status: "PUBLISHED", publishedAt: new Date() },
    create: {
      slug: "claude-code-prisma-demo",
      title: "Claude Code Prisma demo",
      body: "Seeded article for local verification.",
      status: "PUBLISHED",
      publishedAt: new Date(),
      authorId: user.id,
    },
  });
}

main().finally(async () => prisma.$disconnect());
npm run db:migrate -- --name init_blog
npm run db:generate
npm run db:seed
npm run test

Checklist: constraints e índices conectados a pantallas reales, SQL sin cambios destructivos accidentales, paginación con límite, transactions cortas, seed idempotente, tests de rollback o doble ejecución, y producción conmigrate deploy.

Casos de uso, errores y CTA

Tres casos típicos: gestión editorial, panel SaaS multi-tenant y activación de permisos tras un pago. El primero necesita slug y estado de publicación; el segundo exige tenant scope en cada query; el tercero requiere idempotencia, audit log y transaction clara.

En una prueba de una pequeña API de blog, Masa hizo que Claude Code explicara los riesgos de migration antes de escribir la app. Eso sacó dos problemas temprano: cascade demasiado amplio y demasiados campos devueltos en la lista pública. Ese es el uso correcto: acelerar, pero verificar con tipos, SQL y tests.

Para estudiar por cuenta propia, revisa /products/. Para equipos que quieren un workflow común de revisión de DB con Claude Code, mira /training/. Con un schema real o un PR generado por IA, la conversación puede ir directo a riesgos concretos y mejoras aplicables.

#Claude Code #Prisma #ORM #database #TypeScript
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.