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 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.
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.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Repo-Audit-Checkliste vor der ersten Änderung
Ein 20-Minuten-Audit für Scope, Risikobereiche, Prüfbefehle und Umsatz-CTA vor der ersten Änderung.
Claude Code Harness Lite: ein kleiner Sicherheitsrahmen für erste Änderungen
Ein einfacher Ablauf, der Lesen, Ändern, Beweise, öffentliche URLs und Umsatz-CTAs trennt.
Claude Code Repo Map im ersten Durchlauf: vorhandenen Code sicher lesen
Sicherer erster Durchlauf für bestehende Repositories mit Claude Code: Repo-Map, kleine Aufgaben, Beweise, Gratis-PDF, Gumroad und Beratung.