Source profileQuality 88/100Review permissions

affaan-m/ECC/skills/prisma-patterns/SKILL.md

prisma-patterns

Prisma ORM patterns for TypeScript backends — schema design, query optimization, transactions, pagination, and critical traps like updateMany returning count not records, $transaction timeouts, migrate dev resetting the DB, @updatedAt skipped on bulk writes, and serverless connection exhaustion.

Source repository stars
234,327
Declared platforms
1
Static risk flags
1
Last source update
2026-07-27
Source checked
2026-07-28

Decision brief

What it does—and where it fits

Production patterns and non-obvious traps for Prisma ORM in TypeScript backends.

Best for

    Not for

    • updateMany returns a count, not records
    • $transaction interactive form times out after 5 seconds

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorDeclaredSource recordInstall path and trigger
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/affaan-m/ECC --skill "skills/prisma-patterns"
    Safe inspection promptEditorial

    Inspect the Agent Skill "prisma-patterns" from https://github.com/affaan-m/ECC/blob/4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38/skills/prisma-patterns/SKILL.md at commit 4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Breaking schema changes require multi-step migration

      Adding NOT NULL to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:

      Adding NOT NULL to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:
    2. 02

      Step 1: create migration locally, then deploy

      Review the “Step 1: create migration locally, then deploy” section in the pinned source before continuing.

      Review and apply the “Step 1: create migration locally, then deploy” source section.
    3. 03

      Step 3: create the NOT NULL constraint migration locally, then deploy

      // GOOD await prisma.post.updateMany({ where: { authorId }, data: { published: true, updatedAt: new Date() }, }); ts // BAD: returns soft-deleted user const user = await prisma.user.findUniqueOrThrow({ where: { id } });

      nestjs-patterns — NestJS service layer that integrates Prismapostgres-patterns — PostgreSQL-level indexing and connection tuningdatabase-migrations — multi-step migration planning for production
    4. 04

      When to Activate

      Designing or modifying Prisma schema models and relations

      Designing or modifying Prisma schema models and relationsWriting queries, transactions, or pagination logicUsing updateMany, deleteMany, or any bulk operation
    5. 05

      Core Concepts

      Never return raw Prisma entities from API responses — map to response DTOs to control exposed fields:

      Add @@index on every foreign key and column used in WHERE or ORDER BY.Declare deletedAt DateTime? upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.updatedAt @updatedAt is set automatically by Prisma on update and upsert only (see Anti-Patterns for bulk update trap).

    Permission review

    Static risk signals and limitations

    Runs scripts

    medium · line 298

    The documentation asks the agent to run terminal commands or scripts.

    npx prisma migrate dev --name add_column

    Runs scripts

    medium · line 301

    The documentation asks the agent to run terminal commands or scripts.

    npx prisma migrate deploy

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score88/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars234,327SourceRepository attention, not individual Skill quality
    Compatibility1 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    affaan-m/ECC
    Skill path
    skills/prisma-patterns/SKILL.md
    Commit
    4e973d3eaf92d97f8d2e2d8abb39d8bdc8711b38
    License
    MIT
    Collected
    2026-07-28
    Default branch
    main
    View the original SKILL.md

    Prisma Patterns

    Production patterns and non-obvious traps for Prisma ORM in TypeScript backends.

    Check your version before applying patterns. The Prisma API surface has evolved across major releases:

    npx prisma --version
    

    Notable API differences across versions:

    • relationJoins can load relations via JOIN rather than separate queries, but may cause row explosion on large 1:N relations or deep include — benchmark both approaches
    • omit field modifier and prisma.$extends Client Extensions API were added
    • Newer installs: the package may be named prisma instead of @prisma/client; PrismaClient may require a driver adapter (e.g. @prisma/adapter-pg); datasource.url may live in prisma.config.ts instead of schema.prisma
    • CLI commands (migrate dev, migrate deploy, generate) are unchanged across versions

    When to Activate

    • Designing or modifying Prisma schema models and relations
    • Writing queries, transactions, or pagination logic
    • Using updateMany, deleteMany, or any bulk operation
    • Running or planning database migrations
    • Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)
    • Implementing soft delete or multi-tenant row filtering

    Core Concepts

    ID Strategy

    StrategyUse WhenAvoid When
    @default(cuid())Default choice — URL-safe, sortable, no collisionsSequential IDs needed for external systems
    @default(uuid())Interoperability with non-Prisma systems requiredHigh-write tables (random UUIDs fragment B-tree indexes)
    @default(autoincrement())Internal join tables, audit logsPublic-facing IDs (exposes record count)

    Schema Defaults

    model User {
      id        String    @id @default(cuid())
      email     String    @unique  // @unique already creates an index — no @@index needed
      name      String
      role      Role      @default(USER)
      posts     Post[]
      createdAt DateTime  @default(now())
      updatedAt DateTime  @updatedAt
      deletedAt DateTime?
    
      @@index([createdAt])
      @@index([deletedAt, createdAt]) // composite for soft-delete + sort queries
    }
    
    • Add @@index on every foreign key and column used in WHERE or ORDER BY.
    • Declare deletedAt DateTime? upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.
    • updatedAt @updatedAt is set automatically by Prisma on update and upsert only (see Anti-Patterns for bulk update trap).

    include vs select

    includeselect
    ReturnsAll scalar fields + specified relationsOnly specified fields
    Use whenYou need most fields plus a relationHot paths, large tables, avoiding over-fetch
    PerformanceMay over-fetch on wide tablesMinimal payload, faster on large datasets
    Prisma 5 noteUses JOIN by default (relationJoins)Same
    // include — all columns + relation
    const user = await prisma.user.findUnique({
      where: { id },
      include: { posts: { select: { id: true, title: true } } },
    });
    
    // select — explicit allowlist
    const user = await prisma.user.findUnique({
      where: { id },
      select: { id: true, email: true, name: true },
    });
    

    Never return raw Prisma entities from API responses — map to response DTOs to control exposed fields:

    // BAD: leaks passwordHash, deletedAt, internal fields
    return await prisma.user.findUniqueOrThrow({ where: { id } });
    
    // GOOD: explicit DTO mapping
    const user = await prisma.user.findUniqueOrThrow({ where: { id } });
    return { id: user.id, name: user.name, email: user.email };
    

    Transaction Form Selection

    SituationUse
    Independent operations, no inter-dependencyArray form
    Later step depends on earlier resultInteractive form
    External calls (email, HTTP) involvedOutside transaction entirely
    // Array form — batched in one round trip
    const [user, post] = await prisma.$transaction([
      prisma.user.update({ where: { id }, data: { name } }),
      prisma.post.create({ data: { title, authorId: id } }),
    ]);
    
    // Interactive form — use tx client only, never the outer 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

    Each PrismaClient instance opens its own connection pool. Instantiate once.

    // lib/prisma.ts
    
    // Option A — adapter-based initialization (required by newer Prisma installs)
    import { PrismaClient } from '@prisma/client'; // or the generated client path for your 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 — direct initialization (older installs, no adapter needed)
    // import { PrismaClient } from '@prisma/client';
    // export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });
    

    Use Option A if your Prisma install requires an adapter argument in the PrismaClient constructor. Use Option B if new PrismaClient() works without arguments. Let the compiler tell you which is correct.

    The globalThis pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).

    N+1 Problem

    Loading relations inside a loop issues one query per row.

    // BAD: N+1 — one extra query per user
    const users = await prisma.user.findMany();
    for (const user of users) {
      const posts = await prisma.post.findMany({ where: { authorId: user.id } });
    }
    
    // GOOD: single query
    const users = await prisma.user.findMany({ include: { posts: true } });
    

    With Prisma 5+ relationJoins, the include form uses a single JOIN. On large 1:N sets this may increase result set size — benchmark both approaches if the relation can return many rows per parent.

    Code Examples

    Cursor Pagination (preferred for feeds and large datasets)

    async function getPosts(cursor?: string, limit = 20) {
      const items = await prisma.post.findMany({
        where: { published: true },
        orderBy: [
          { createdAt: 'desc' },
          { id: 'desc' }, // secondary sort prevents unstable pagination on duplicate timestamps
        ],
        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 };
    }
    

    Fetch limit + 1 and pop — canonical way to detect hasNextPage without an extra count query. Always include a unique field (e.g. id) as a secondary orderBy to prevent unstable pagination when multiple rows share the same timestamp. Use offset pagination only when users need to jump to arbitrary pages (admin tables).

    Soft Delete

    // Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)
    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 } }); // restore
    

    Error Handling

    import { Prisma } from '@prisma/client'; // or the generated client path for your 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;
    }
    

    Common codes: P2002 unique violation · P2025 not found · P2003 foreign key violation.

    Catch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.

    Connection Pool — Serverless

    Embed connection params directly in DATABASE_URL — string concatenation breaks if the URL already has query parameters (e.g. ?schema=public):

    # .env — preferred: embed params in the URL
    DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1&pool_timeout=20"
    
    # With an external pooler (PgBouncer, Supabase pooler)
    DATABASE_URL="postgresql://user:pass@host/db?pgbouncer=true&connection_limit=1"
    
    // Vercel, AWS Lambda, and similar serverless runtimes:
    // cap pool to 1 per instance; connection_limit and pool_timeout controlled via DATABASE_URL
    
    // Adapter-based setup (if your Prisma install requires an adapter):
    import { PrismaClient } from '@prisma/client';
    import { PrismaPg } from '@prisma/adapter-pg';
    
    const prisma = new PrismaClient({
      adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
    });
    
    // Direct setup (if your Prisma install does not require an adapter):
    // const prisma = new PrismaClient();
    

    Anti-Patterns

    updateMany returns a count, not records

    // BAD: result is { count: 2 } — users[0] is undefined
    const users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });
    
    // GOOD: capture IDs first, then update, then fetch only the affected rows
    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 } } });
    

    Same applies to deleteMany — returns { count: n }, never the deleted rows.

    $transaction interactive form times out after 5 seconds

    // BAD: external call inside transaction exceeds 5s default → "Transaction already closed"
    await prisma.$transaction(async (tx) => {
      const user = await tx.user.findUniqueOrThrow({ where: { id } });
      await sendWelcomeEmail(user.email); // external call
      await tx.user.update({ where: { id }, data: { emailSent: true } });
    });
    
    // GOOD: external calls outside the transaction
    const user = await prisma.user.findUniqueOrThrow({ where: { id } });
    await sendWelcomeEmail(user.email);
    await prisma.user.update({ where: { id }, data: { emailSent: true } });
    
    // Only raise timeout when bulk processing genuinely needs it
    await prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });
    

    migrate dev can reset the database

    migrate dev detects schema drift and may prompt to reset the DB, dropping all data.

    # NEVER on shared dev, staging, or production
    npx prisma migrate dev --name add_column
    
    # Safe everywhere except local solo dev
    npx prisma migrate deploy
    
    # Check drift without applying
    npx prisma migrate diff \
      --from-migrations ./prisma/migrations \
      --to-schema-datamodel ./prisma/schema.prisma \
      --shadow-database-url "$SHADOW_DATABASE_URL"
    

    Manually editing a migration file breaks future deploys

    Prisma checksums every migration file. Editing after apply causes P3006 checksum mismatch on every environment where the original already ran. Create a new migration instead.

    Breaking schema changes require multi-step migration

    Adding NOT NULL to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:

    # Step 1: create migration locally, then deploy
    npx prisma migrate dev --name add_new_column   # local only
    npx prisma migrate deploy                       # staging / production
    
    // Step 2: backfill data (run in a script or migration job, not in the shell)
    await prisma.user.updateMany({ data: { newColumn: derivedValue } });
    
    # Step 3: create the NOT NULL constraint migration locally, then deploy
    npx prisma migrate dev --name make_new_column_required  # local only
    npx prisma migrate deploy                               # staging / production
    

    @updatedAt does not fire on updateMany

    @updatedAt is set automatically only on update and upsert. Bulk writes leave it stale.

    // BAD: updatedAt stays at its old value
    await prisma.post.updateMany({ where: { authorId }, data: { published: true } });
    
    // GOOD
    await prisma.post.updateMany({
      where: { authorId },
      data: { published: true, updatedAt: new Date() },
    });
    

    Soft delete + findUniqueOrThrow leaks deleted records

    findUniqueOrThrow throws P2025 only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.

    findUniqueOrThrow requires a unique constraint field in where — adding deletedAt: null alongside id breaks the type because { id, deletedAt } is not a compound unique constraint. Use findFirstOrThrow instead.

    // BAD: returns soft-deleted user
    const user = await prisma.user.findUniqueOrThrow({ where: { id } });
    
    // BAD: Prisma type error — { id, deletedAt } is not a unique constraint
    const user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });
    
    // GOOD: findFirstOrThrow supports arbitrary where conditions
    const user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });
    

    deleteMany without where deletes every row

    // BAD: silently wipes the table
    await prisma.post.deleteMany();
    
    // GOOD
    await prisma.post.deleteMany({ where: { authorId: userId } });
    

    Best Practices

    RuleReason
    migrate deploy in CI/CD, migrate dev only locallymigrate dev can reset the DB on drift
    Map entities to response DTOsPrevents leaking internal fields
    Catch PrismaClientKnownRequestError at service boundaryTranslate to domain errors
    Prefer *OrThrow methods over manual null checksThrows P2025 automatically; use findFirstOrThrow when filtering non-unique fields
    connection_limit=1 + external pooler in serverlessPrevents connection exhaustion
    Always provide where on deleteManyPrevents accidental table wipe
    Set updatedAt: new Date() manually in updateMany@updatedAt skips bulk writes

    Related Skills

    • nestjs-patterns — NestJS service layer that integrates Prisma
    • postgres-patterns — PostgreSQL-level indexing and connection tuning
    • database-migrations — multi-step migration planning for production
    • backend-patterns — general API and service layer design

    Alternatives

    Compare before choosing

    Computed 95165

    JasonColapietro/suede-creator-skills

    suede-code-grader

    Give a blunt A-F ship grade for a code change across correctness, security, data, UX, verification, and deploy readiness. Use for a grade, not a findings review.

    Computed 9438,313

    wshobson/agents

    brand-landingpage

    Brand-first landing page designer — runs a brand-identity interview (colors, typography, shape language), then generates and iterates on a polished landing page via Stitch with deployment-ready HTML. Use when the user asks to create, design, or build a landing page, homepage, or marketing page and has no established visual direction. Skip when they have a design mockup, need a dashboard or app UI, are working at component level, building a multi-page app, or restyling with known design tokens —

    Computed 9137,126

    github/awesome-copilot

    foundry-hosted-agent-copilotkit

    Ongoing development guidance for agentic web apps that pair a CopilotKit frontend with Microsoft Agent Framework agents on Azure AI Foundry hosted agents over the AG-UI protocol - add and gate agent tools, wire human-in-the-loop approvals, build generative UI and shared state, debug the event stream, upgrade pre-1.0 packages safely, and deploy hosted agent updates.

    Computed 8938,313

    wshobson/agents

    deployment-pipeline-design

    Design multi-stage CI/CD pipelines with approval gates, security checks, and deployment orchestration. Use this skill when designing zero-downtime deployment pipelines, implementing canary rollout strategies, setting up multi-environment promotion workflows, or debugging failed deployment gates in CI/CD.