Prisma ORM dengan Claude Code: schema, migration, transaction, dan test
Bangun Prisma ORM dengan Claude Code: desain schema, review migration, transactions, seed/test, dan checklist produksi.
Prisma ORM adalah lapisan database bertipe untuk mengakses DB secara aman dari TypeScript. Dengan Claude Code, Prisma bisa menjadi workflow lengkap: desain schema, review SQL migration, implementasi Prisma Client, pembatasan transaction, seed, test, dan checklist sebelum rilis.
Masalah muncul ketika Prisma diperlakukan sebagai generator CRUD otomatis. Kode mungkin jalan, tetapi index bisa kurang,includebisa terlalu besar, cascade delete bisa berbahaya, dan migration bisa gagal saat bertemu data produksi. Artikel ini memakai contoh API blog untuk menunjukkan cara membuat data layer yang layak dipublikasikan dan mudah direview.
Rujukan resmi: Prisma ORM, Prisma Schema, Transactions, dan Prisma Migrate. Artikel terkait: Claude Code getting started, Drizzle ORM, Supabase integration, dan Redis caching.
Workflow
flowchart LR
A["Berikan aturan produk ke Claude Code"] --> B["Desain schema.prisma"]
B --> C["Generate dan review migration.sql"]
C --> D["Implementasi Prisma Client"]
D --> E["Tambahkan seed dan test"]
E --> F["Checklist sebelum produksi"]
Pisahkan pekerjaan menjadi tiga artefak: schema, SQL migration, dan kode TypeScript. Claude Code boleh membuat draft, tetapi constraint, index, aturan delete, dan transaction boundary tetap harus direview manusia.
Prompt untuk Claude Code
Desain data layer Prisma ORM untuk API blog.
Konteks:
- TypeScript + Prisma ORM + SQLite lokal, nanti mungkin pindah ke PostgreSQL
- Model: User, Post, Category, Comment, Notification, AuditLog
- Post memakai status string: DRAFT/PUBLISHED/ARCHIVED
- email dan slug harus unique
- Public listing filter berdasarkan status, publishedAt, author, category
- Saat Post dihapus, Comment dan join rows ikut cascade delete
- User delete dibuat Restrict dulu; anonymization menjadi migration terpisah
Kembalikan:
1. prisma/schema.prisma
2. Poin review untuk migration SQL
3. Kode Prisma Client create/list/publish
4. Command seed dan test
5. Checklist produksi
Prompt yang baik menjelaskan niat operasional. Claude Code perlu tahu data apa yang boleh dihapus, query mana yang paling sering dipakai, dan operasi mana yang harus atomic.
Setup lokal
{
"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])
}
Minta Claude Code menjelaskan setiap index. status + publishedAt mendukung daftar publik, sedangkan postId + createdAt mendukung tampilan komentar. Index tanpa query nyata hanya menambah biaya maintenance.
Review migration
npx prisma format
npx prisma migrate dev --create-only --name init_blog
# Review prisma/migrations/*/migration.sql di PR
npx prisma migrate dev
npx prisma generate
Untuk produksi:
npx prisma migrate deploy
db push nyaman untuk prototype, tetapi bukan workflow produksi. PeriksaDROP, penambahanNOT NULL, unique constraint pada data lama, dan perubahan cascade sebelum deploy.
Panduan rollback harus ditulis di PR sebelum deploy. Jika migration sudah sukses, biasanya lebih aman membuat migration baru yang membalik perubahan. Jika migration gagal di tengah jalan, ikuti workflow down migration Prisma: cek backup, jalankan migrate status, terapkan down SQL yang sudah direview, lalu tandai hanya migration yang gagal dengan migrate resolve.
npx prisma migrate status
npx prisma migrate resolve --rolled-back "20260603090000_failed_change"
Prisma Client dan transaction
// 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;
});
}
Gunakan$transaction([])untuk read independen seperti list dan count. Gunakan interactive transaction ketika satu aksi bisnis harus mengubah beberapa tabel sekaligus. Jangan menaruh email, webhook, atau panggilan API lambat di dalam transaction.
Safe SQL
Mulai dari Prisma Client query. Jika laporan benar-benar butuh raw SQL, pitfall utamanya adalah string concatenation karena membuka SQL-injection risk. Dokumentasi raw queries Prisma menyarankan tagged templates atau 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)}
`);
}
Minta Claude Code menjelaskan setiap penggunaan $queryRawUnsafe. Untuk nama table atau column dinamis, pakai allowlist yang direview manusia.
Seed, test, dan 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: constraint dan index sesuai layar nyata, SQL tidak destruktif tanpa sengaja, pagination punya batas, transaction singkat, seed idempotent, test mencakup rollback atau eksekusi ganda, dan produksi memakaimigrate deploy.
Use case, pitfall, dan CTA
Tiga use case utama adalah manajemen artikel, dashboard SaaS multi-tenant, dan pemberian akses setelah pembayaran. Situs konten butuh slug dan status yang konsisten. SaaS butuh tenant scope di setiap query. Payment flow butuh idempotency, audit log, dan transaction yang jelas.
Saat Masa menguji flow ini pada API blog kecil, meminta Claude Code menjelaskan risiko migration sebelum menulis aplikasi langsung membuka dua masalah: cascade terlalu luas dan public list mengembalikan field author yang tidak perlu. Prisma dengan Claude Code paling kuat saat AI mempercepat draft, lalu type, SQL, dan tests memverifikasi hasilnya.
Untuk belajar mandiri, mulai dari /products/. Untuk tim yang butuh workflow review DB bersama Claude Code, lihat /training/. Bawa schema atau PR hasil AI, lalu diskusi bisa langsung masuk ke risiko produksi yang konkret.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Checklist audit repo pertama Claude Code sebelum edit pertama
Audit repo 20 menit untuk scope, area risiko, command bukti, dan CTA revenue sebelum edit pertama.
Claude Code Harness Lite: pagar kecil untuk perubahan pemula
Workflow ringan untuk memisahkan baca repo, edit, bukti, URL publik, dan CTA revenue di Claude Code.
Claude Code Repo Map First Pass: membaca codebase lama tanpa membuang konteks
Cara aman membaca repository lama dengan Claude Code sebelum edit: repo map, task kecil, bukti, PDF gratis, Gumroad, dan konsultasi.