Getting Started (Aktualisiert: 3.6.2026)

Prisma ORM mit Claude Code: Schema, Migrationen, Transactions und Tests

Prisma ORM mit Claude Code umsetzen: Schema-Design, Migration-Review, Transactions, Seed/Test und Review-Checklist.

Prisma ORM mit Claude Code: Schema, Migrationen, Transactions und Tests

Prisma ORM ist eine typisierte DB-Schicht, mit der du Datenbanken aus TypeScript sicher verwendest. In Kombination mit Claude Code kann daraus ein sauberer Workflow entstehen: Schema entwerfen, Migration-SQL prüfen, Prisma Client verwenden, Transactions begrenzen und Seed/Test-Befehle ergänzen.

Der Fehler liegt oft darin, Prisma als reinen CRUD-Generator zu behandeln. Dann entstehen fehlende Indexe, zu breiteinclude-Abfragen, unklare Cascade-Regeln oder Migrationen, die lokal funktionieren, aber mit Produktionsdaten scheitern. Dieser Leitfaden zeigt an einer Blog-API, wie du Claude Code produktiv nutzt, ohne die Datenbankkontrolle abzugeben.

Offizielle Referenzen: Prisma ORM, Prisma Schema, Transactions und Prisma Migrate. Passende interne Artikel sind der Claude Code Einstieg, Drizzle ORM Guide, Supabase Integration und Redis Caching.

Workflow

flowchart LR
  A["Produktregeln an Claude Code geben"] --> B["schema.prisma entwerfen"]
  B --> C["migration.sql erzeugen und prüfen"]
  C --> D["Prisma Client Queries schreiben"]
  D --> E["Seed und Tests ergänzen"]
  E --> F["Review-Checklist vor Release"]

Denke in drei Artefakten: Schema, SQL-Migration und TypeScript-Code. Claude Code darf sie erzeugen, aber Constraints, Indexe, Löschregeln und Transaction-Grenzen müssen bewusst geprüft werden.

Prompt für Claude Code

Entwirf eine Prisma ORM Datenschicht für eine Blog-API.

Rahmen:
- TypeScript + Prisma ORM + SQLite lokal, später PostgreSQL möglich
- Modelle: User, Post, Category, Comment, Notification, AuditLog
- Post hat status als String: DRAFT/PUBLISHED/ARCHIVED
- email und slug sind unique
- Die öffentliche Liste filtert nach status, publishedAt, author, category
- Beim Löschen eines Post werden Comment und Join-Zeilen per Cascade gelöscht
- User-Löschung ist zunächst Restrict; Anonymisierung kommt als eigene Migration

Liefere:
1. prisma/schema.prisma
2. Review-Punkte für migration SQL
3. Prisma Client create/list/publish Code
4. seed- und test-Befehle
5. Review-Checklist für Produktion

Die wichtigen Angaben sind nicht nur Modellnamen, sondern Betriebsregeln. Wenn Claude Code erklären muss, warum ein Index existiert und warum ein Delete eingeschränkt ist, wird die Ausgabe deutlich belastbarer.

Lokales Setup

{
  "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-Beispiel

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

@@index([status, publishedAt]) gehört zur öffentlichen Artikelliste. @@index([postId, createdAt]) gehört zur Kommentaransicht. Bitte Claude Code um eine kurze Begründung für jeden Index; so erkennst du schnell Ballast.

Migrationen prüfen

npx prisma format
npx prisma migrate dev --create-only --name init_blog
# prisma/migrations/*/migration.sql im Pull Request lesen
npx prisma migrate dev
npx prisma generate

Für Produktion:

npx prisma migrate deploy

db push ist für Prototypen nützlich, ersetzt aber kein reviewbares Migration-Log. Prüfe besondersDROP, neueNOT NULL-Spalten, unique Constraints auf bestehende Daten und Cascade-Regeln.

Rollback gehört vor dem Deployment in die PR-Beschreibung. Wenn eine Migration schon erfolgreich war, ist meistens eine neue Vorwärtsmigration besser als ein manuelles Zurückdrehen. Bei einer fehlgeschlagenen Migration folgst du dem offiziellen Down-Migration-Workflow: Backup prüfen, migrate status ausführen, geprüftes Down-SQL anwenden und nur die fehlgeschlagene Migration mit migrate resolve als rolled back markieren.

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

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

Nutze$transaction([]) für unabhängige Leseoperationen wie Liste plus Count. Nutze eine interactive transaction, wenn ein Geschäftsvorgang mehrere Tabellen konsistent verändern muss. Keine E-Mails, Webhooks oder langsamen Netzwerkanfragen innerhalb der Transaction.

Safe SQL

Nimm zuerst Prisma Client Queries. Wenn eine Reporting-Abfrage wirklich SQL braucht, ist String-Verkettung der zentrale pitfall und ein SQL-injection risk. Laut Prisma Raw Queries solltest du tagged templates oder Prisma.sql verwenden.

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

Lass Claude Code jede Nutzung von $queryRawUnsafe begründen. Für dynamische Tabellen- oder Spaltennamen ist eine menschlich geprüfte Allowlist nötig.

Seed, Test und Review

// 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 und Indexe passen zu echten Screens; SQL enthält keine unbeabsichtigte Zerstörung; Listen begrenzentake; Transactions bleiben kurz; Seed ist idempotent; Tests decken Rollback und doppelte Ausführung ab; Produktion nutztmigrate deploy.

Use Cases, Fallstricke und CTA

Prisma ORM passt gut zu Content-Verwaltung, SaaS-Adminflächen und Entitlement-Flows nach Zahlungen. Content braucht slug, Status und Kategoriefilter. SaaS braucht Tenant-Scope in jeder Query. Zahlungsflows brauchen Idempotenz, AuditLog und klare Transaction-Grenzen.

Masa hat in einer kleinen Blog-API gemerkt, dass ein früher Migration-Review mit Claude Code sofort zwei Fehler sichtbar machte: zu breite Cascade-Löschung und zu viele Felder in der öffentlichen Liste. Genau dort lohnt sich die Kombination aus AI-Tempo und menschlichem Datenbank-Review.

Für Selbststudium eignen sich die Materialien unter /products/. Wenn ein Team einen gemeinsamen Prisma-review workflow braucht, passt /training/. Am effektivsten ist ein Gespräch mit echtem Schema, Migration-Historie oder AI-generiertem Pull Request.

#Claude Code #Prisma #ORM #database #TypeScript
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.