Getting Started (Mis à jour: 03/06/2026)

Prisma ORM avec Claude Code : schema, migrations, transactions et tests

Guide Prisma ORM avec Claude Code : schema, migrations, transactions, seed/test et checklist production.

Prisma ORM avec Claude Code : schema, migrations, transactions et tests

Prisma ORM est une couche de base de données typée pour manipuler une DB en sécurité depuis TypeScript. Avec Claude Code, il peut devenir un vrai flux de travail : concevoir le schema, générer et relire la migration SQL, écrire les requêtes Prisma Client, poser les limites de transaction, puis ajouter seed et tests.

Le piège consiste à le traiter comme un simple générateur CRUD. On obtient alors du code qui démarre, mais parfois avec des index absents, desinclude trop larges, des cascades dangereuses ou une migration qui échoue dès qu’il y a des données réelles. Ce guide utilise une API de blog pour montrer une approche publiable et monétisable, adaptée à un site de contenu ou à un outil SaaS.

Références officielles : Prisma ORM, Prisma Schema, Transactions et Prisma Migrate. Côté ClaudeCodeLab, consulte aussi le guide de démarrage Claude Code, Drizzle ORM, Supabase et Redis caching.

Vue d’ensemble

flowchart LR
  A["Donner les règles métier à Claude Code"] --> B["Concevoir schema.prisma"]
  B --> C["Générer et relire migration.sql"]
  C --> D["Écrire les requêtes Prisma Client"]
  D --> E["Ajouter seed et tests"]
  E --> F["Valider la checklist production"]

Découpe le travail en trois livrables : schema, migration SQL et code TypeScript. Claude Code peut tout préparer, mais la validation des contraintes, index, suppressions et transactions reste une responsabilité humaine.

Prompt pour Claude Code

Conçois une couche Prisma ORM pour une API de blog.

Contexte:
- TypeScript + Prisma ORM + SQLite en local, migration possible vers PostgreSQL
- Modèles: User, Post, Category, Comment, Notification, AuditLog
- Post utilise un status string: DRAFT/PUBLISHED/ARCHIVED
- email et slug doivent être uniques
- La liste publique filtre par status, publishedAt, author, category
- Supprimer un Post supprime en cascade Comment et les lignes de jointure
- Supprimer un User est interdit pour l'instant; l'anonymisation viendra dans une migration séparée

Retour attendu:
1. prisma/schema.prisma
2. Points de review pour la migration SQL
3. Code Prisma Client create/list/publish
4. Commandes seed et test
5. Checklist avant production

Ce prompt force Claude Code à raisonner sur l’exploitation, pas seulement sur les tables. Les règles de suppression et les index sont liés à des écrans et à des risques métier.

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

Demande à Claude Code d’expliquer chaque index. status + publishedAt sert la liste publique; postId + createdAt sert les commentaires. Un index sans requête réelle derrière lui est un coût inutile.

Relire la migration

npx prisma format
npx prisma migrate dev --create-only --name init_blog
# Lire prisma/migrations/*/migration.sql dans la PR
npx prisma migrate dev
npx prisma generate

En production :

npx prisma migrate deploy

db push est utile en prototype, pas comme stratégie de production. Surveille lesDROP, les nouveauxNOT NULL, les unique constraints sur données existantes et les cascades.

La stratégie de rollback doit être écrite dans la PR avant le déploiement. Si la migration a déjà réussi, crée plutôt une nouvelle migration qui annule le changement. Si elle a échoué en cours de route, suis le workflow officiel des down migrations : vérifier le backup, lancer migrate status, appliquer le SQL de retour relu, puis marquer seulement cette migration comme rolled back avec migrate resolve.

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

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

$transaction([]) convient aux lectures indépendantes comme liste et count. Une transaction interactive convient lorsqu’une action métier modifie plusieurs tables. Garde-la courte : pas d’e-mail, pas de webhook, pas de requête réseau lente dedans.

SQL sûr

Commence par les requêtes Prisma Client. Si un reporting a vraiment besoin de SQL, le pitfall est la concaténation de chaînes, qui crée un SQL-injection risk. La page Raw queries de Prisma recommande les 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)}
  `);
}

Demande à Claude Code de justifier chaque $queryRawUnsafe. Pour des noms de table ou colonne dynamiques, impose une allowlist relue par un humain.

Seed, test et 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 : contraintes et index alignés avec les écrans, SQL non destructif, pagination bornée, transactions courtes, seed idempotent, tests de rollback ou de double exécution, production avecmigrate deploy.

Cas d’usage, pièges et CTA

Trois cas d’usage reviennent souvent : gestion d’articles, back-office SaaS et activation de droits après paiement. Le premier demande slug et statut fiables; le second impose le scope tenant; le troisième exige idempotence, audit log et transaction claire.

Dans un test de petite API de blog, Masa a demandé à Claude Code de lister les risques de migration avant d’écrire l’application. Deux problèmes sont sortis immédiatement : cascade trop large et trop de champs renvoyés dans la liste publique. C’est le bon usage de Prisma avec Claude Code : accélérer, puis vérifier par le type, le SQL et les tests.

Pour apprendre seul, commence par /products/. Pour un workflow d’équipe autour des revues DB avec Claude Code, regarde /training/. Une session avec votre schema réel ou une PR générée par IA permet d’aller directement aux risques concrets.

#Claude Code #Prisma #ORM #database #TypeScript
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.