Communitygithub.com

Prisma-Patterns

Prisma ORM-Muster für TypeScript-Backends — Schema-Design, Query-Optimierung, Transaktionen, Paginierung und kritische Fallstricke wie updateMany, das einen Count statt Datensätzen zurückgibt, $transaction-Timeouts, migrate dev, das die DB zurücksetzt, @updatedAt, das bei Bulk-Schreibvorgängen übersprungen wird, und Verbindungserschöpfung in serverlosen Umgebungen. Verwende diese Muster beim Schreiben eines Prisma-Schemas oder einer Abfrage oder beim Debuggen von Transaktionen, Migrationen oder serverlosen Verbindungslimits.

Was ist Prisma-Patterns?

Diese Skill-Sammlung enthält erprobte Produktionsmuster und nicht offensichtliche Fallstricke für die Verwendung von Prisma ORM in TypeScript-Backends. Sie deckt eine breite Palette von Themen ab, darunter Schema-Design mit ID-Strategien (cuid, uuid, autoincrement), die Wahl zwischen `include` und `select` zur Optimierung von Abfragen, verschiedene Transaktionsformen (Array vs. interaktiv) und die korrekte Implementierung von Cursor-Paginierung für Feeds und große Datenmengen. Ein Schwerpunkt liegt auf häufigen Fehlern: `updateMany` und `deleteMany` geben lediglich einen Zähler zurück, nicht die betroffenen Datensätze; `@updatedAt` wird bei Bulk-Operationen nicht automatisch gesetzt; die interaktive `$transaction`-Form hat ein Standard-Timeout von 5 Sekunden; und `migrate dev` kann Datenbanken zurücksetzen, wenn Schemaabweichungen erkannt werden. Es werden auch fortgeschrittene Themen wie Soft Deletion in Kombination mit `findUniqueOrThrow` (das gelöschte Datensätze nicht erfasst) und die korrekte Handhabung von Verbindungspools in serverlosen Umgebungen wie Vercel oder AWS Lambda behandelt. Die Muster sind plattformunabhängig und gelten für alle Prisma-basierten Projekte (Next.js, Express, Fastify, etc.). Sie sind besonders wertvoll für Entwickler, die Prisma-Versionsunterschiede navigieren müssen, da die Skill-Referenzen auf API-Änderungen wie `relationJoins`, `omit` und die Client-Extensions-API eingehen.

Funktioniert mit~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/prisma-patterns

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Prisma-Muster

Produktionsmuster und nicht offensichtliche Fallstricke für Prisma ORM in TypeScript-Backends.

Prüfe deine Version, bevor du Muster anwendest. Die Prisma-API-Oberfläche hat sich über mehrere Hauptversionen hinweg weiterentwickelt:

npx prisma --version

Bemerkenswerte API-Unterschiede zwischen den Versionen:

  • relationJoins kann Beziehungen über JOIN anstelle separater Abfragen laden, kann aber bei großen 1:N-Beziehungen oder tiefem include zu einer Zeilenexplosion führen — bewerte beide Ansätze
  • Der Feldmodifikator omit und die Client Extensions API prisma.$extends wurden hinzugefügt
  • Neuere Installationen: Das Paket kann prisma anstelle von @prisma/client heißen; PrismaClient erfordert möglicherweise einen Treiber-Adapter (z. B. @prisma/adapter-pg); datasource.url kann sich in prisma.config.ts anstelle von schema.prisma befinden
  • CLI-Befehle (migrate dev, migrate deploy, generate) sind versionsübergreifend unverändert

Wann aktivieren

  • Entwerfen oder Ändern von Prisma-Schema-Modellen und -Beziehungen
  • Schreiben von Abfragen, Transaktionen oder Paginierungslogik
  • Verwenden von updateMany, deleteMany oder anderen Bulk-Operationen
  • Ausführen oder Planen von Datenbankmigrationen
  • Bereitstellen in serverlosen Umgebungen (Vercel, Lambda, Cloudflare Workers)
  • Implementieren von Soft-Deletion oder Multi-Tenant-Zeilenfilterung

Kernkonzepte

ID-Strategie

StrategieVerwendung, wennVermeiden, wenn
@default(cuid())Standardwahl — URL-sicher, sortierbar, keine KollisionenFortlaufende IDs für externe Systeme benötigt werden
@default(uuid())Interoperabilität mit Nicht-Prisma-Systemen erforderlichTabellen mit vielen Schreibvorgängen (zufällige UUIDs fragmentieren B-Tree-Indizes)
@default(autoincrement())Interne Joins-Tabellen, Audit-LogsÖffentlich sichtbare IDs (gibt die Anzahl der Datensätze preis)

Schema-Standardwerte

model User {
  id        String    @id @default(cuid())
  email     String    @unique  // @unique erstellt bereits einen Index — kein @@index nötig
  name      String
  role      Role      @default(USER)
  posts     Post[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  deletedAt DateTime?

  @@index([createdAt])
  @@index([deletedAt, createdAt]) // zusammengesetzt für Soft-Delete + Sortierabfragen
}
  • Füge @@index auf jedem Fremdschlüssel und jeder Spalte hinzu, die in WHERE oder ORDER BY verwendet wird.
  • Deklariere deletedAt DateTime? frühzeitig, wenn Soft-Deletion absehbar ist — ein späteres Hinzufügen erfordert eine Migration auf einer Live-Tabelle.
  • updatedAt @updatedAt wird von Prisma nur bei update und upsert automatisch gesetzt (siehe Anti-Patterns für die Bulk-Update-Falle).

include vs. select

includeselect
RückgabeAlle skalaren Felder + angegebene BeziehungenNur angegebene Felder
Verwenden, wennDie meisten Felder plus eine Beziehung benötigt werdenKritische Pfade, große Tabellen, Vermeidung von Überabruf
LeistungKann bei breiten Tabellen zu Überabruf führenMinimale Nutzlast, schneller bei großen Datensätzen
Prisma 5 HinweisVerwendet standardmäßig JOIN (relationJoins)Gleich
// include — alle Spalten + Beziehung
const user = await prisma.user.findUnique({
  where: { id },
  include: { posts: { select: { id: true, title: true } } },
});

// select — explizite Erlaubnisliste
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, email: true, name: true },
});

Gib niemals rohe Prisma-Entitäten aus API-Antworten zurück — bilde sie auf Antwort-DTOs ab, um exponierte Felder zu kontrollieren:

// SCHLECHT: gibt passwordHash, deletedAt, interne Felder preis
return await prisma.user.findUniqueOrThrow({ where: { id } });

// GUT: explizites DTO-Mapping
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };

Transaktionsformular-Auswahl

SituationVerwende
Unabhängige Operationen, keine AbhängigkeitenArray-Form
Späterer Schritt hängt von früherem Ergebnis abInteraktive Form
Externe Aufrufe (E-Mail, HTTP) beteiligtVollständig außerhalb der Transaktion
// Array-Form — gebündelt in einem Roundtrip
const [user, post] = await prisma.$transaction([
  prisma.user.update({ where: { id }, data: { name } }),
  prisma.post.create({ data: { title, authorId: id } }),
]);

// Interaktive Form — verwende nur den tx-Client, niemals den äußeren prisma-Client
const post = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title, authorId: user.id } });
});

PrismaClient-Singleton

Jede PrismaClient-Instanz öffnet ihren eigenen Verbindungspool. Einmal instanziieren.

// lib/prisma.ts

// Option A — adapterbasierte Initialisierung (erforderlich für neuere Prisma-Installationen)
import { PrismaClient } from '@prisma/client'; // oder den generierten Client-Pfad für dein Setup
import { PrismaPg } from '@prisma/adapter-pg';

function createPrismaClient() {
  const adapter = new PrismaPg({
    connectionString: process.env.DATABASE_URL!,
  });
  return new PrismaClient({
    adapter,
    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  });
}

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

// Option B — direkte Initialisierung (ältere Installationen, kein Adapter nötig)
// import { PrismaClient } from '@prisma/client';
// export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });

Verwende Option A, wenn deine Prisma-Installation ein adapter-Argument im PrismaClient-Konstruktor erfordert. Verwende Option B, wenn new PrismaClient() ohne Argumente funktioniert. Lass den Compiler entscheiden, was korrekt ist.

Das globalThis-Muster verhindert doppelte Instanzen während des Hot Reloads (Next.js, nodemon, ts-node-dev).

N+1-Problem

Das Laden von Beziehungen innerhalb einer Schleife führt zu einer Abfrage pro Zeile.

// SCHLECHT: N+1 — eine zusätzliche Abfrage pro Benutzer
const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

// GUT: einzelne Abfrage
const users = await prisma.user.findMany({ include: { posts: true } });

Mit relationJoins in Prisma 5+ verwendet die include-Form einen einzelnen JOIN. Bei großen 1:N-Mengen kann dies die Ergebnismenge vergrößern — bewerte beide Ansätze, wenn die Beziehung viele Zeilen pro Elternteil zurückgeben kann.

Codebeispiele

Cursor-Paginierung (bevorzugt für Feeds und große Datensätze)

async function getPosts(cursor?: string, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // sekundäre Sortierung verhindert instabile Paginierung bei doppelten Zeitstempeln
    ],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };
}

Hole limit + 1 und entferne das letzte Element — kanonischer Weg, um hasNextPage ohne eine zusätzliche COUNT-Abfrage zu erkennen. Füge immer ein eindeutiges Feld (z. B. id) als sekundäres orderBy hinzu, um instabile Paginierung zu verhindern, wenn mehrere Zeilen denselben Zeitstempel haben. Verwende Offset-Paginierung nur, wenn Benutzer zu beliebigen Seiten springen müssen (Admin-Tabellen).

Soft Delete

// Immer explizit filtern — sich nicht auf Middleware verlassen (verbirgt das Verhalten, schwer zu debuggen)
const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });

await prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });
await prisma.user.update({ where: { id }, data: { deletedAt: null } }); // wiederherstellen

Fehlerbehandlung

import { Prisma } from '@prisma/client'; // oder den generierten Client-Pfad für dein Setup

try {
  await prisma.user.create({ data: { email } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('Email already exists');
    if (e.code === 'P2025') throw new NotFoundError('Record not found');
    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');
  }
  throw e;
}

Häufige Codes: P2002 Unique-Verletzung · P2025 nicht gefunden · P2003 Fremdschlüsselverletzung.

Fange an der Servicegrenze ab und übersetze in domänenspezifische Fehler. Gib niemals rohe Prisma-Nachrichten an API-Konsumenten weiter.

Verbindungspool — Serverlos

Bette Verbindungsparameter direkt in DATABASE_URL ein — Zeichenkettenverkettung bricht, wenn die URL bereits Abfrageparameter hat (z. B. ?schema=public):

# .env — bevorzugt: Parameter in die URL einbetten
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"

# Mit einem externen Pooler (PgBouncer, Supabase-Pooler)
DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
// Vercel, AWS Lambda und ähnliche serverlose Laufzeiten:
// Pool auf 1 pro Instanz begrenzen; connection_limit und pool_timeover über DATABASE_URL gesteuert

// Adapterbasierte Einrichtung (falls deine Prisma-Installation einen Adapter erfordert):
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});

// Direkte Einrichtung (falls deine Prisma-Installation keinen Adapter erfordert):
// const prisma = new PrismaClient();

Anti-Patterns

updateMany gibt einen Zähler zurück, keine Datensätze

// SCHLECHT: Ergebnis ist { count: 2 } — users[0] ist undefined
const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });

// GUT: zuerst IDs erfassen, dann aktualisieren, dann nur die betroffenen Zeilen abrufen
const targets = await prisma.user.findMany({
  where: { role: 'GUEST' },
  select: { id: true },
});
const ids = targets.map((u) => u.id);
await prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });
const updated = await prisma.user.findMany({ where: { id: { in: ids } } });

Gleiches gilt für deleteMany — gibt { count: n } zurück, niemals die gelöschten Zeilen.

$transaction interaktive Form läuft nach 5 Sekunden ab

// SCHLECHT: externer Aufruf innerhalb der Transaktion überschreitet das Standard-Timeout von 5s → "Transaction already closed"
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id } });
  await sendWelcomeEmail(user.email); // externer Aufruf
  await tx.user.update({ where: { id }, data: { emailSent: true } });
});

// GUT: externe Aufrufe außerhalb der Transaktion
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
await sendWelcomeEmail(user.email);
await prisma.user.update({ where: { id }, data: { emailSent: true } });

// Erhöhe das Timeout nur, wenn die Bulk-Verarbeitung es wirklich erfordert
await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });

migrate dev kann die Datenbank zurücksetzen

migrate dev erkennt Schema-Abweichungen und kann dazu auffordern, die DB zurückzusetzen, wobei alle Daten gelöscht werden.

# NIEMALS auf gemeinsam genutzten Entwicklungs-, Staging- oder Produktionsumgebungen
npx prisma migrate dev --name add_column

# Sicher überall außer bei lokaler Solo-Entwicklung
npx prisma migrate deploy

# Überprüfe Abweichungen ohne Anwendung
npx prisma migrate diff \
  --from-migrations ./prisma/migrations \
  --to-schema-datamodel ./prisma/schema.prisma \
  --shadow-database-url "$SHADOW_DATABASE_URL"

Manuelles Bearbeiten einer Migrationsdatei bricht zukünftige Bereitstellungen

Prisma prüft die Prüfsumme jeder Migrationsdatei. Das Bearbeiten nach der Anwendung verursacht P3006 checksum mismatch in jeder Umgebung, in der die ursprüngliche bereits ausgeführt wurde. Erstelle stattdessen eine neue Migration.

Schemaänderungen, die bestehende Daten brechen, erfordern mehrstufige Migrationen

Das Hinzufügen von NOT NULL zu einer vorhandenen Spalte oder das Umbenennen einer Spalte in einer Migration sperrt die Tabelle oder löscht Daten. Verwende Expand-and-Contract:

# Schritt 1: Migration lokal erstellen, dann bereitstellen
npx prisma migrate dev --name add_new_column   # nur lokal
npx prisma migrate deploy                       # Staging / Produktion
// Schritt 2: Daten nachträglich befüllen (in einem Skript oder Migrations-Job ausführen, nicht in der Shell)
await prisma.user.updateMany({ data: { newColumn: derivedValue } });
# Schritt 3: NOT NULL-Constraint-Migration lokal erstellen, dann bereitstellen
npx prisma migrate dev --name make_new_column_required  # nur lokal
npx prisma migrate deploy                               # Staging / Produktion

@updatedAt wird bei updateMany nicht ausgelöst

@updatedAt wird nur bei update und upsert automatisch gesetzt. Bulk-Schreibvorgänge lassen es veraltet.

// SCHLECHT: updatedAt bleibt beim alten Wert
await prisma.post.updateMany({ where: { authorId }, data: { published: true } });

// GUT
await prisma.post.updateMany({
  where: { authorId },
  data: { published: true, updatedAt: new Date() },
});

Soft Delete + findUniqueOrThrow gibt gelöschte Datensätze preis

findUniqueOrThrow wirft P2025 nur, wenn die Zeile nicht in der DB existiert. Soft-gelöschte Zeilen existieren noch und werden ohne Fehler zurückgegeben.

findUniqueOrThrow erfordert ein Feld mit einer Unique-Constraint in where — das Hinzufügen von deletedAt: null neben id bricht den Typ, da { id, deletedAt } keine zusammengesetzte Unique-Constraint ist. Verwende stattdessen findFirstOrThrow.

// SCHLECHT: gibt einen soft-gelöschten Benutzer zurück
const user = await prisma.user.findUniqueOrThrow({ where: { id } });

// SCHLECHT: Prisma-Typfehler — { id, deletedAt } ist keine Unique-Constraint
const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });

// GUT: findFirstOrThrow unterstützt beliebige WHERE-Bedingungen
const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });

deleteMany ohne where löscht jede Zeile

// SCHLECHT: löscht stillschweigend die gesamte Tabelle
await prisma.post.deleteMany();

// GUT
await prisma.post.deleteMany({ where: { authorId: userId } });

Best Practices

RegelGrund
migrate deploy in CI/CD, migrate dev nur lokalmigrate dev kann die DB bei Abweichungen zurücksetzen
Entitäten auf Antwort-DTOs abbildenVerhindert das Preisgeben interner Felder
Cursor-Paginierung für Feeds und große DatensätzeOffset-Paginierung wird bei großen Versätzen langsam (teures OFFSET)
Immer deletedAt: null in where für Soft-Delete angebenfindUniqueOrThrow filtert nicht automatisch nach deletedAt
Verbindungspool auf 1 in serverlosen Umgebungen begrenzenVerhindert Verbindungserschöpfung bei gleichzeitigen Aufrufen
Verwende findFirstOrThrow anstelle von findUniqueOrThrow für Soft-DeletefindUniqueOrThrow erlaubt keine zusätzlichen Nicht-Constraint-Felder in where
updateMany + @updatedAt manuell setzen@updatedAt wird bei Bulk-Operationen nicht automatisch aktualisiert
Transaktionen kurz halten (< 5s)Das Standard-Timeout der interaktiven Transaktion beträgt 5 Sekunden
Fremdschlüssel immer indexieren (@@index)Verhindert sequentielle Scans bei Joins und kaskadierenden Löschvorgängen

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

Verwandte Skills