Getting Started (Atualizado: 03/06/2026)

Prisma ORM com Claude Code: schema, migrations, transactions e testes

Implemente Prisma ORM com Claude Code: schema, revisão de migration, transactions, seed/test e checklist de produção.

Prisma ORM com Claude Code: schema, migrations, transactions e testes

Prisma ORM é uma camada tipada de banco de dados para acessar a DB com segurança a partir de TypeScript. Com Claude Code, ele vira um fluxo completo: desenhar o schema, gerar e revisar SQL de migration, escrever queries com Prisma Client, limitar transactions e criar seed/test.

O risco é tratar Prisma como um gerador automático de CRUD. Isso costuma produzir código que roda, mas com índices ausentes,include grande demais, cascade perigoso ou migration que quebra quando encontra dados reais. Este guia usa uma API de blog para mostrar um caminho revisável e bom o suficiente para sites de conteúdo, SaaS e ferramentas internas.

Use as fontes oficiais como referência: Prisma ORM, Prisma Schema, Transactions e Prisma Migrate. No ClaudeCodeLab, veja também guia inicial de Claude Code, Drizzle ORM, Supabase e Redis caching.

Fluxo de trabalho

flowchart LR
  A["Regras do produto para Claude Code"] --> B["Desenhar schema.prisma"]
  B --> C["Gerar e revisar migration.sql"]
  C --> D["Implementar Prisma Client"]
  D --> E["Adicionar seed e testes"]
  E --> F["Checklist antes de produção"]

Separe a entrega em schema, SQL migration e código TypeScript. Claude Code pode criar o rascunho, mas constraints, índices, regras de deleção e fronteiras de transaction precisam de revisão humana.

Prompt para Claude Code

Projete uma camada Prisma ORM para uma API de blog.

Contexto:
- TypeScript + Prisma ORM + SQLite local, com possível migração futura para PostgreSQL
- Modelos: User, Post, Category, Comment, Notification, AuditLog
- Post usa status string: DRAFT/PUBLISHED/ARCHIVED
- email e slug devem ser únicos
- A lista pública filtra por status, publishedAt, author e category
- Ao deletar um Post, Comment e linhas de junção são removidos em cascade
- Deletar User fica restrito; anonimização será outra migration

Entregue:
1. prisma/schema.prisma
2. Pontos de revisão da migration SQL
3. Código Prisma Client para create/list/publish
4. Comandos de seed e test
5. Checklist de produção

Um bom prompt descreve a operação real, não só os nomes das tabelas. Ele explica o que é único, o que não pode ser apagado e quais telas precisam de índices.

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])
}

Peça para Claude Code justificar cada índice. status + publishedAt atende a lista pública; postId + createdAt atende comentários. Índice sem query real por trás é custo.

Revisão de migration

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

Em produção:

npx prisma migrate deploy

db push serve para protótipo, não para produção. Antes de publicar, reviseDROP, novosNOT NULL, unique constraints sobre dados existentes e cascade.

A orientação de rollback deve estar na PR antes do deploy. Se a migration já passou, em geral é melhor criar outra migration para desfazer o schema change. Se ela falhou no meio, siga o workflow oficial de down migrations: conferir backup, rodar migrate status, aplicar SQL revisado se necessário e marcar só a migration falha com migrate resolve.

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

Prisma Client e 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;
  });
}

Use$transaction([]) para leituras independentes, como lista e count. Use interactive transaction quando uma operação de negócio precisa alterar várias tabelas de forma atômica. Não faça email, webhook ou chamada lenta de rede dentro da transaction.

SQL seguro

Comece por Prisma Client queries. Quando uma consulta de relatório realmente precisa de SQL, o pitfall é concatenar strings, criando SQL-injection risk. A documentação de raw queries do Prisma recomenda tagged templates ou Prisma.sql.

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)}
  `);
}

Peça para Claude Code justificar cada $queryRawUnsafe. Para nomes dinâmicos de tabela ou coluna, use uma allowlist revisada por uma pessoa.

Seed, test e 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 ligados a telas reais, SQL sem mudança destrutiva acidental, paginação com limite, transactions curtas, seed idempotente, testes de rollback ou repetição, produção commigrate deploy.

Casos de uso, erros e CTA

Três usos comuns: gestão editorial, painel SaaS multi-tenant e liberação de permissão após pagamento. Conteúdo precisa de slug e status confiáveis; SaaS precisa de tenant scope em toda query; pagamentos precisam de idempotência, audit log e transaction clara.

Em um teste com uma pequena API de blog, Masa pediu para Claude Code explicar os riscos de migration antes de escrever a aplicação. Isso revelou cedo um cascade amplo demais e retorno de campos desnecessários na lista pública. Esse é o melhor uso: acelerar com IA e verificar com tipos, SQL e testes.

Para estudo individual, comece em /products/. Para equipes que precisam de workflow de review de DB com Claude Code, veja /training/. Com um schema real ou PR gerado por IA, a conversa começa nos riscos concretos.

#Claude Code #Prisma #ORM #database #TypeScript
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.