[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JDCKNTHH0TX8PW9FS3CSTQ":3,"skill-same:01M6JDCKNTHH0TX8PW9FS3CSTQ":66},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":49,"ownerHtmlUrl":50,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":51,"install":52,"resources":65},"01M6JDCKNTHH0TX8PW9FS3CSTQ","prisma-patterns",null,"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.","S3YED\u002Fappie-kit","S3YED","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F62743867?v=4","01M6JBKBRRVP6KMS0VWZ27T3R6","01M6JBKBX0KG30284PW1DBYJT1",14,"MIT","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fecc\u002Fprisma-patterns\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"patterns","prisma patterns","prisma","query","typescript","for","prisma orm","bulk","records","critical","db","dev","Optimization","design","schema design","query optimization","schema","orm","migrate","transactions","2026-11-10 10:12:30",false,"---\nname: prisma-patterns\ndescription: 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.\norigin: ECC\n---\n\n# Prisma Patterns\n\nProduction patterns and non-obvious traps for Prisma ORM in TypeScript backends.\nTested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.\n\nCheck the Prisma version before applying version-specific patterns:\n\n```bash\nnpx prisma --version\n```\n\nPrisma 5 introduced `relationJoins`, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The `omit` field modifier and `prisma.$extends` Client Extensions API were also added. Note: `relationJoins` can cause row explosion on large 1:N relations or deep nested `include` — benchmark both approaches when relations may return many rows per parent.\n\n## When to Activate\n\n- Designing or modifying Prisma schema models and relations\n- Writing queries, transactions, or pagination logic\n- Using `updateMany`, `deleteMany`, or any bulk operation\n- Running or planning database migrations\n- Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)\n- Implementing soft delete or multi-tenant row filtering\n\n## Core Concepts\n\n### ID Strategy\n\n| Strategy | Use When | Avoid When |\n|---|---|---|\n| `@default(cuid())` | Default choice — URL-safe, sortable, no collisions | Sequential IDs needed for external systems |\n| `@default(uuid())` | Interoperability with non-Prisma systems required | High-write tables (random UUIDs fragment B-tree indexes) |\n| `@default(autoincrement())` | Internal join tables, audit logs | Public-facing IDs (exposes record count) |\n\n### Schema Defaults\n\n```prisma\nmodel User {\n  id        String    @id @default(cuid())\n  email     String    @unique  \u002F\u002F @unique already creates an index — no @@index needed\n  name      String\n  role      Role      @default(USER)\n  posts     Post[]\n  createdAt DateTime  @default(now())\n  updatedAt DateTime  @updatedAt\n  deletedAt DateTime?\n\n  @@index([createdAt])\n  @@index([deletedAt, createdAt]) \u002F\u002F composite for soft-delete + sort queries\n}\n```\n\n- Add `@@index` on every foreign key and column used in `WHERE` or `ORDER BY`.\n- Declare `deletedAt DateTime?` upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.\n- `updatedAt @updatedAt` is set automatically by Prisma on `update` and `upsert` only (see Anti-Patterns for bulk update trap).\n\n### `include` vs `select`\n\n| | `include` | `select` |\n|---|---|---|\n| Returns | All scalar fields + specified relations | Only specified fields |\n| Use when | You need most fields plus a relation | Hot paths, large tables, avoiding over-fetch |\n| Performance | May over-fetch on wide tables | Minimal payload, faster on large datasets |\n| Prisma 5 note | Uses JOIN by default (`relationJoins`) | Same |\n\n```ts\n\u002F\u002F include — all columns + relation\nconst user = await prisma.user.findUnique({\n  where: { id },\n  include: { posts: { select: { id: true, title: true } } },\n});\n\n\u002F\u002F select — explicit allowlist\nconst user = await prisma.user.findUnique({\n  where: { id },\n  select: { id: true, email: true, name: true },\n});\n```\n\nNever return raw Prisma entities from API responses — map to response DTOs to control exposed fields:\n\n```ts\n\u002F\u002F BAD: leaks passwordHash, deletedAt, internal fields\nreturn await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F GOOD: explicit DTO mapping\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nreturn { id: user.id, name: user.name, email: user.email };\n```\n\n### Transaction Form Selection\n\n| Situation | Use |\n|---|---|\n| Independent operations, no inter-dependency | Array form |\n| Later step depends on earlier result | Interactive form |\n| External calls (email, HTTP) involved | Outside transaction entirely |\n\n```ts\n\u002F\u002F Array form — batched in one round trip\nconst [user, post] = await prisma.$transaction([\n  prisma.user.update({ where: { id }, data: { name } }),\n  prisma.post.create({ data: { title, authorId: id } }),\n]);\n\n\u002F\u002F Interactive form — use tx client only, never the outer prisma client\nconst post = await prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  if (user.role !== 'ADMIN') throw new Error('Forbidden');\n  return tx.post.create({ data: { title, authorId: user.id } });\n});\n```\n\n### PrismaClient Singleton\n\nEach `PrismaClient` instance opens its own connection pool. Instantiate once.\n\n```ts\n\u002F\u002F lib\u002Fprisma.ts\nimport { PrismaClient } from '@prisma\u002Fclient';\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma =\n  globalForPrisma.prisma ??\n  new PrismaClient({\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n```\n\nThe `globalThis` pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).\n\n### N+1 Problem\n\nLoading relations inside a loop issues one query per row.\n\n```ts\n\u002F\u002F BAD: N+1 — one extra query per user\nconst users = await prisma.user.findMany();\nfor (const user of users) {\n  const posts = await prisma.post.findMany({ where: { authorId: user.id } });\n}\n\n\u002F\u002F GOOD: single query\nconst users = await prisma.user.findMany({ include: { posts: true } });\n```\n\nWith 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.\n\n## Code Examples\n\n### Cursor Pagination (preferred for feeds and large datasets)\n\n```ts\nasync function getPosts(cursor?: string, limit = 20) {\n  const items = await prisma.post.findMany({\n    where: { published: true },\n    orderBy: [\n      { createdAt: 'desc' },\n      { id: 'desc' }, \u002F\u002F secondary sort prevents unstable pagination on duplicate timestamps\n    ],\n    take: limit + 1,\n    ...(cursor && { cursor: { id: cursor }, skip: 1 }),\n  });\n\n  const hasNextPage = items.length > limit;\n  if (hasNextPage) items.pop();\n\n  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };\n}\n```\n\nFetch `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).\n\n### Soft Delete\n\n```ts\n\u002F\u002F Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)\nconst activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });\n\nawait prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });\nawait prisma.user.update({ where: { id }, data: { deletedAt: null } }); \u002F\u002F restore\n```\n\n### Error Handling\n\n```ts\nimport { Prisma } from '@prisma\u002Fclient';\n\ntry {\n  await prisma.user.create({ data: { email } });\n} catch (e) {\n  if (e instanceof Prisma.PrismaClientKnownRequestError) {\n    if (e.code === 'P2002') throw new ConflictError('Email already exists');\n    if (e.code === 'P2025') throw new NotFoundError('Record not found');\n    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');\n  }\n  throw e;\n}\n```\n\nCommon codes: `P2002` unique violation · `P2025` not found · `P2003` foreign key violation.\n\nCatch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.\n\n### Connection Pool — Serverless\n\nEmbed connection params directly in `DATABASE_URL` — string concatenation breaks if the URL already has query parameters (e.g. `?schema=public`):\n\n```bash\n# .env — preferred: embed params in the URL\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?connection_limit=1&pool_timeout=20\"\n\n# With an external pooler (PgBouncer, Supabase pooler)\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?pgbouncer=true&connection_limit=1\"\n```\n\n```ts\n\u002F\u002F Vercel, AWS Lambda, and similar serverless runtimes: cap pool to 1 per instance\n\u002F\u002F connection_limit and pool_timeout are controlled via DATABASE_URL\nconst prisma = new PrismaClient();\n```\n\n## Anti-Patterns\n\n### `updateMany` returns a count, not records\n\n```ts\n\u002F\u002F BAD: result is { count: 2 } — users[0] is undefined\nconst users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });\n\n\u002F\u002F GOOD: capture IDs first, then update, then fetch only the affected rows\nconst targets = await prisma.user.findMany({\n  where: { role: 'GUEST' },\n  select: { id: true },\n});\nconst ids = targets.map((u) => u.id);\nawait prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });\nconst updated = await prisma.user.findMany({ where: { id: { in: ids } } });\n```\n\nSame applies to `deleteMany` — returns `{ count: n }`, never the deleted rows.\n\n### `$transaction` interactive form times out after 5 seconds\n\n```ts\n\u002F\u002F BAD: external call inside transaction exceeds 5s default → \"Transaction already closed\"\nawait prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  await sendWelcomeEmail(user.email); \u002F\u002F external call\n  await tx.user.update({ where: { id }, data: { emailSent: true } });\n});\n\n\u002F\u002F GOOD: external calls outside the transaction\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nawait sendWelcomeEmail(user.email);\nawait prisma.user.update({ where: { id }, data: { emailSent: true } });\n\n\u002F\u002F Only raise timeout when bulk processing genuinely needs it\nawait prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });\n```\n\n### `migrate dev` can reset the database\n\n`migrate dev` detects schema drift and may prompt to reset the DB, dropping all data.\n\n```bash\n# NEVER on shared dev, staging, or production\nnpx prisma migrate dev --name add_column\n\n# Safe everywhere except local solo dev\nnpx prisma migrate deploy\n\n# Check drift without applying\nnpx prisma migrate diff \\\n  --from-migrations .\u002Fprisma\u002Fmigrations \\\n  --to-schema-datamodel .\u002Fprisma\u002Fschema.prisma \\\n  --shadow-database-url \"$SHADOW_DATABASE_URL\"\n```\n\n### Manually editing a migration file breaks future deploys\n\nPrisma checksums every migration file. Editing after apply causes `P3006 checksum mismatch` on every environment where the original already ran. Create a new migration instead.\n\n### Breaking schema changes require multi-step migration\n\nAdding `NOT NULL` to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:\n\n```bash\n# Step 1: create migration locally, then deploy\nnpx prisma migrate dev --name add_new_column   # local only\nnpx prisma migrate deploy                       # staging \u002F production\n```\n\n```ts\n\u002F\u002F Step 2: backfill data (run in a script or migration job, not in the shell)\nawait prisma.user.updateMany({ data: { newColumn: derivedValue } });\n```\n\n```bash\n# Step 3: create the NOT NULL constraint migration locally, then deploy\nnpx prisma migrate dev --name make_new_column_required  # local only\nnpx prisma migrate deploy                               # staging \u002F production\n```\n\n### `@updatedAt` does not fire on `updateMany`\n\n`@updatedAt` is set automatically only on `update` and `upsert`. Bulk writes leave it stale.\n\n```ts\n\u002F\u002F BAD: updatedAt stays at its old value\nawait prisma.post.updateMany({ where: { authorId }, data: { published: true } });\n\n\u002F\u002F GOOD\nawait prisma.post.updateMany({\n  where: { authorId },\n  data: { published: true, updatedAt: new Date() },\n});\n```\n\n### Soft delete + `findUniqueOrThrow` leaks deleted records\n\n`findUniqueOrThrow` throws `P2025` only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.\n\n`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.\n\n```ts\n\u002F\u002F BAD: returns soft-deleted user\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F BAD: Prisma type error — { id, deletedAt } is not a unique constraint\nconst user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });\n\n\u002F\u002F GOOD: findFirstOrThrow supports arbitrary where conditions\nconst user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });\n```\n\n### `deleteMany` without `where` deletes every row\n\n```ts\n\u002F\u002F BAD: silently wipes the table\nawait prisma.post.deleteMany();\n\n\u002F\u002F GOOD\nawait prisma.post.deleteMany({ where: { authorId: userId } });\n```\n\n## Best Practices\n\n| Rule | Reason |\n|---|---|\n| `migrate deploy` in CI\u002FCD, `migrate dev` only locally | `migrate dev` can reset the DB on drift |\n| Map entities to response DTOs | Prevents leaking internal fields |\n| Catch `PrismaClientKnownRequestError` at service boundary | Translate to domain errors |\n| Prefer `*OrThrow` methods over manual null checks | Throws P2025 automatically; use `findFirstOrThrow` when filtering non-unique fields |\n| `connection_limit=1` + external pooler in serverless | Prevents connection exhaustion |\n| Always provide `where` on `deleteMany` | Prevents accidental table wipe |\n| Set `updatedAt: new Date()` manually in `updateMany` | `@updatedAt` skips bulk writes |\n\n## Related Skills\n\n- `nestjs-patterns` — NestJS service layer that integrates Prisma\n- `postgres-patterns` — PostgreSQL-level indexing and connection tuning\n- `database-migrations` — multi-step migration planning for production\n- `backend-patterns` — general API and service layer design\n","# Prisma Patterns\n\nProduction patterns and non-obvious traps for Prisma ORM in TypeScript backends.\nTested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.\n\nCheck the Prisma version before applying version-specific patterns:\n\n```bash\nnpx prisma --version\n```\n\nPrisma 5 introduced `relationJoins`, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The `omit` field modifier and `prisma.$extends` Client Extensions API were also added. Note: `relationJoins` can cause row explosion on large 1:N relations or deep nested `include` — benchmark both approaches when relations may return many rows per parent.\n\n## When to Activate\n\n- Designing or modifying Prisma schema models and relations\n- Writing queries, transactions, or pagination logic\n- Using `updateMany`, `deleteMany`, or any bulk operation\n- Running or planning database migrations\n- Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)\n- Implementing soft delete or multi-tenant row filtering\n\n## Core Concepts\n\n### ID Strategy\n\n| Strategy | Use When | Avoid When |\n|---|---|---|\n| `@default(cuid())` | Default choice — URL-safe, sortable, no collisions | Sequential IDs needed for external systems |\n| `@default(uuid())` | Interoperability with non-Prisma systems required | High-write tables (random UUIDs fragment B-tree indexes) |\n| `@default(autoincrement())` | Internal join tables, audit logs | Public-facing IDs (exposes record count) |\n\n### Schema Defaults\n\n```prisma\nmodel User {\n  id        String    @id @default(cuid())\n  email     String    @unique  \u002F\u002F @unique already creates an index — no @@index needed\n  name      String\n  role      Role      @default(USER)\n  posts     Post[]\n  createdAt DateTime  @default(now())\n  updatedAt DateTime  @updatedAt\n  deletedAt DateTime?\n\n  @@index([createdAt])\n  @@index([deletedAt, createdAt]) \u002F\u002F composite for soft-delete + sort queries\n}\n```\n\n- Add `@@index` on every foreign key and column used in `WHERE` or `ORDER BY`.\n- Declare `deletedAt DateTime?` upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.\n- `updatedAt @updatedAt` is set automatically by Prisma on `update` and `upsert` only (see Anti-Patterns for bulk update trap).\n\n### `include` vs `select`\n\n| | `include` | `select` |\n|---|---|---|\n| Returns | All scalar fields + specified relations | Only specified fields |\n| Use when | You need most fields plus a relation | Hot paths, large tables, avoiding over-fetch |\n| Performance | May over-fetch on wide tables | Minimal payload, faster on large datasets |\n| Prisma 5 note | Uses JOIN by default (`relationJoins`) | Same |\n\n```ts\n\u002F\u002F include — all columns + relation\nconst user = await prisma.user.findUnique({\n  where: { id },\n  include: { posts: { select: { id: true, title: true } } },\n});\n\n\u002F\u002F select — explicit allowlist\nconst user = await prisma.user.findUnique({\n  where: { id },\n  select: { id: true, email: true, name: true },\n});\n```\n\nNever return raw Prisma entities from API responses — map to response DTOs to control exposed fields:\n\n```ts\n\u002F\u002F BAD: leaks passwordHash, deletedAt, internal fields\nreturn await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F GOOD: explicit DTO mapping\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nreturn { id: user.id, name: user.name, email: user.email };\n```\n\n### Transaction Form Selection\n\n| Situation | Use |\n|---|---|\n| Independent operations, no inter-dependency | Array form |\n| Later step depends on earlier result | Interactive form |\n| External calls (email, HTTP) involved | Outside transaction entirely |\n\n```ts\n\u002F\u002F Array form — batched in one round trip\nconst [user, post] = await prisma.$transaction([\n  prisma.user.update({ where: { id }, data: { name } }),\n  prisma.post.create({ data: { title, authorId: id } }),\n]);\n\n\u002F\u002F Interactive form — use tx client only, never the outer prisma client\nconst post = await prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  if (user.role !== 'ADMIN') throw new Error('Forbidden');\n  return tx.post.create({ data: { title, authorId: user.id } });\n});\n```\n\n### PrismaClient Singleton\n\nEach `PrismaClient` instance opens its own connection pool. Instantiate once.\n\n```ts\n\u002F\u002F lib\u002Fprisma.ts\nimport { PrismaClient } from '@prisma\u002Fclient';\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma =\n  globalForPrisma.prisma ??\n  new PrismaClient({\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n```\n\nThe `globalThis` pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).\n\n### N+1 Problem\n\nLoading relations inside a loop issues one query per row.\n\n```ts\n\u002F\u002F BAD: N+1 — one extra query per user\nconst users = await prisma.user.findMany();\nfor (const user of users) {\n  const posts = await prisma.post.findMany({ where: { authorId: user.id } });\n}\n\n\u002F\u002F GOOD: single query\nconst users = await prisma.user.findMany({ include: { posts: true } });\n```\n\nWith 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.\n\n## Code Examples\n\n### Cursor Pagination (preferred for feeds and large datasets)\n\n```ts\nasync function getPosts(cursor?: string, limit = 20) {\n  const items = await prisma.post.findMany({\n    where: { published: true },\n    orderBy: [\n      { createdAt: 'desc' },\n      { id: 'desc' }, \u002F\u002F secondary sort prevents unstable pagination on duplicate timestamps\n    ],\n    take: limit + 1,\n    ...(cursor && { cursor: { id: cursor }, skip: 1 }),\n  });\n\n  const hasNextPage = items.length > limit;\n  if (hasNextPage) items.pop();\n\n  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };\n}\n```\n\nFetch `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).\n\n### Soft Delete\n\n```ts\n\u002F\u002F Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)\nconst activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });\n\nawait prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });\nawait prisma.user.update({ where: { id }, data: { deletedAt: null } }); \u002F\u002F restore\n```\n\n### Error Handling\n\n```ts\nimport { Prisma } from '@prisma\u002Fclient';\n\ntry {\n  await prisma.user.create({ data: { email } });\n} catch (e) {\n  if (e instanceof Prisma.PrismaClientKnownRequestError) {\n    if (e.code === 'P2002') throw new ConflictError('Email already exists');\n    if (e.code === 'P2025') throw new NotFoundError('Record not found');\n    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');\n  }\n  throw e;\n}\n```\n\nCommon codes: `P2002` unique violation · `P2025` not found · `P2003` foreign key violation.\n\nCatch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.\n\n### Connection Pool — Serverless\n\nEmbed connection params directly in `DATABASE_URL` — string concatenation breaks if the URL already has query parameters (e.g. `?schema=public`):\n\n```bash\n# .env — preferred: embed params in the URL\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?connection_limit=1&pool_timeout=20\"\n\n# With an external pooler (PgBouncer, Supabase pooler)\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?pgbouncer=true&connection_limit=1\"\n```\n\n```ts\n\u002F\u002F Vercel, AWS Lambda, and similar serverless runtimes: cap pool to 1 per instance\n\u002F\u002F connection_limit and pool_timeout are controlled via DATABASE_URL\nconst prisma = new PrismaClient();\n```\n\n## Anti-Patterns\n\n### `updateMany` returns a count, not records\n\n```ts\n\u002F\u002F BAD: result is { count: 2 } — users[0] is undefined\nconst users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });\n\n\u002F\u002F GOOD: capture IDs first, then update, then fetch only the affected rows\nconst targets = await prisma.user.findMany({\n  where: { role: 'GUEST' },\n  select: { id: true },\n});\nconst ids = targets.map((u) => u.id);\nawait prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });\nconst updated = await prisma.user.findMany({ where: { id: { in: ids } } });\n```\n\nSame applies to `deleteMany` — returns `{ count: n }`, never the deleted rows.\n\n### `$transaction` interactive form times out after 5 seconds\n\n```ts\n\u002F\u002F BAD: external call inside transaction exceeds 5s default → \"Transaction already closed\"\nawait prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  await sendWelcomeEmail(user.email); \u002F\u002F external call\n  await tx.user.update({ where: { id }, data: { emailSent: true } });\n});\n\n\u002F\u002F GOOD: external calls outside the transaction\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nawait sendWelcomeEmail(user.email);\nawait prisma.user.update({ where: { id }, data: { emailSent: true } });\n\n\u002F\u002F Only raise timeout when bulk processing genuinely needs it\nawait prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });\n```\n\n### `migrate dev` can reset the database\n\n`migrate dev` detects schema drift and may prompt to reset the DB, dropping all data.\n\n```bash\n# NEVER on shared dev, staging, or production\nnpx prisma migrate dev --name add_column\n\n# Safe everywhere except local solo dev\nnpx prisma migrate deploy\n\n# Check drift without applying\nnpx prisma migrate diff \\\n  --from-migrations .\u002Fprisma\u002Fmigrations \\\n  --to-schema-datamodel .\u002Fprisma\u002Fschema.prisma \\\n  --shadow-database-url \"$SHADOW_DATABASE_URL\"\n```\n\n### Manually editing a migration file breaks future deploys\n\nPrisma checksums every migration file. Editing after apply causes `P3006 checksum mismatch` on every environment where the original already ran. Create a new migration instead.\n\n### Breaking schema changes require multi-step migration\n\nAdding `NOT NULL` to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:\n\n```bash\n# Step 1: create migration locally, then deploy\nnpx prisma migrate dev --name add_new_column   # local only\nnpx prisma migrate deploy                       # staging \u002F production\n```\n\n```ts\n\u002F\u002F Step 2: backfill data (run in a script or migration job, not in the shell)\nawait prisma.user.updateMany({ data: { newColumn: derivedValue } });\n```\n\n```bash\n# Step 3: create the NOT NULL constraint migration locally, then deploy\nnpx prisma migrate dev --name make_new_column_required  # local only\nnpx prisma migrate deploy                               # staging \u002F production\n```\n\n### `@updatedAt` does not fire on `updateMany`\n\n`@updatedAt` is set automatically only on `update` and `upsert`. Bulk writes leave it stale.\n\n```ts\n\u002F\u002F BAD: updatedAt stays at its old value\nawait prisma.post.updateMany({ where: { authorId }, data: { published: true } });\n\n\u002F\u002F GOOD\nawait prisma.post.updateMany({\n  where: { authorId },\n  data: { published: true, updatedAt: new Date() },\n});\n```\n\n### Soft delete + `findUniqueOrThrow` leaks deleted records\n\n`findUniqueOrThrow` throws `P2025` only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.\n\n`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.\n\n```ts\n\u002F\u002F BAD: returns soft-deleted user\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F BAD: Prisma type error — { id, deletedAt } is not a unique constraint\nconst user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });\n\n\u002F\u002F GOOD: findFirstOrThrow supports arbitrary where conditions\nconst user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });\n```\n\n### `deleteMany` without `where` deletes every row\n\n```ts\n\u002F\u002F BAD: silently wipes the table\nawait prisma.post.deleteMany();\n\n\u002F\u002F GOOD\nawait prisma.post.deleteMany({ where: { authorId: userId } });\n```\n\n## Best Practices\n\n| Rule | Reason |\n|---|---|\n| `migrate deploy` in CI\u002FCD, `migrate dev` only locally | `migrate dev` can reset the DB on drift |\n| Map entities to response DTOs | Prevents leaking internal fields |\n| Catch `PrismaClientKnownRequestError` at service boundary | Translate to domain errors |\n| Prefer `*OrThrow` methods over manual null checks | Throws P2025 automatically; use `findFirstOrThrow` when filtering non-unique fields |\n| `connection_limit=1` + external pooler in serverless | Prevents connection exhaustion |\n| Always provide `where` on `deleteMany` | Prevents accidental table wipe |\n| Set `updatedAt: new Date()` manually in `updateMany` | `@updatedAt` skips bulk writes |\n\n## Related Skills\n\n- `nestjs-patterns` — NestJS service layer that integrates Prisma\n- `postgres-patterns` — PostgreSQL-level indexing and connection tuning\n- `database-migrations` — multi-step migration planning for production\n- `backend-patterns` — general API and service layer design\n",{"name":5,"origin":43,"description":7},"ECC","master","8ec37df6d71fb664ea19d32c079e7d3ff931a8e0","skills\u002Fecc\u002Fprisma-patterns",3,"https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit","https:\u002F\u002Fweblyfe.ai","https:\u002F\u002Fgithub.com\u002FS3YED","2026-08-27 01:54:02",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add S3YED\u002Fappie-kit --skill prisma-patterns",{"tool":58,"command":59},"bunx","bunx skills add S3YED\u002Fappie-kit --skill prisma-patterns",{"tool":61,"command":62},"pnpm","pnpm dlx skills add S3YED\u002Fappie-kit --skill prisma-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fecc\u002Fprisma-patterns\u002FSKILL.md",true,[],[67,103,137,163,194,218],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":84,"publishedAt":38},"01M6JFD4M6N4DSGC7TXEJ5R3J2","ai-music","AI music generation and audio tools: HeartMuLa lyrics-to-music generation, songwriting craft (+ Suno prompts), and audio spectrogram\u002Ffeature visualization.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fwethlete\u002Fai-music\u002FSKILL.md",[73,78,81],{"categoryNo":74,"categoryName":75,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000013","分析与可视化","SCENARIO","使用场景",{"categoryNo":79,"categoryName":80,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":82,"categoryName":83,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000023","Skill与智能体开发",[85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],"music","ai music","humanizer","visualization","Tools","Audio","hermes","hermes agent","songwriting","suno","lyrics","prompts","heartmula","craft","generation","feature","ai music generation","music generation",{"skillNo":104,"name":105,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":117,"publishedAt":38},"01M6JFCZE4XNFQFMNZDHKHT5V5","agentic-webdesign-design-system-ingestion","Use v0 Design Systems 2.0, Bolt Design System Agents, or Lovable to generate production UI from a client's real design system components — not generic Tailwind. Use when building a client website where brand consistency matters.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-design-system-ingestion\u002FSKILL.md",[109,112],{"categoryNo":110,"categoryName":111,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":113,"categoryName":114,"taxonomyType":115,"taxonomyTypeDesc":116},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[31,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136],"agentic","design system","system","ingestion","webdesign","agentic webdesign design system ingestion","components","generate","design systems","website","ui","Tailwind","bolt","lovable","v0","design-system","wed","design system components","appie",{"skillNo":138,"name":139,"nameZh":6,"description":140,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":141,"categories":142,"keywords":145,"publishedAt":38},"01M6JFCVPX3913CFAS6QQJ33ZB","agentic-webdesign-agent-elements","Use 21st.dev Agent Elements — 25 shadcn\u002Fui components purpose-built for AI agent UIs. Tool cards (Bash, Edit, Search, Plan, Subagent, MCP), chat, input, streaming. Open source, shadcn-registry install, no runtime lock-in.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-agent-elements\u002FSKILL.md",[143,144],{"categoryNo":82,"categoryName":83,"taxonomyType":76,"taxonomyTypeDesc":77},{"categoryNo":113,"categoryName":114,"taxonomyType":115,"taxonomyTypeDesc":116},[118,146,122,147,148,149,124,150,151,152,153,154,155,156,157,158,159,160,161,162],"elements","agentic webdesign agent elements","MCP","ai agent","registry","search","streaming","open source","open","Bash","plan","subagent","edit","install","input","lock","source",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":173,"publishedAt":38},"01M6JFCQC7H65M9QJ6TRE71BS1","vercel-deployment","Deploy static sites and frontends to Vercel — handles both CLI and REST API paths, including the CLI token sanitization workaround.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fvercel-deployment\u002FSKILL.md",[169,172],{"categoryNo":170,"categoryName":171,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":82,"categoryName":83,"taxonomyType":76,"taxonomyTypeDesc":77},[174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193],"deployment","vercel deployment","vercel","macos","cli","linux","windows","deploy","vercel deploy","REST","api","site","deploy to vercel","token","rest api","host","sites","paths","sanitization","static site",{"skillNo":195,"name":196,"nameZh":6,"description":197,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":198,"categories":199,"keywords":201,"publishedAt":38},"01M6JFCM4SMTXBM5A01BK2YQG1","software-quality-workflows","Umbrella workflow for TDD, debugging, code simplification, spikes, pre-commit review, and debugger-assisted investigation.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fsoftware-quality-workflows\u002FSKILL.md",[200],{"categoryNo":82,"categoryName":83,"taxonomyType":76,"taxonomyTypeDesc":77},[202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,23],"workflows","quality","software","software quality workflows","review","tdd","workflow","commit","debugging","code","debugger","pre-commit","investigation","code simplification","user","simplification",{"skillNo":219,"name":220,"nameZh":6,"description":221,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":222,"categories":223,"keywords":231,"publishedAt":38},"01M6JFCGD0M6CEATY01W7YN6DB","react-component-refactoring","Safely refactor large React components into smaller, maintainable pieces using proven patterns","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Freact-component-refactoring\u002FSKILL.md",[224,227,230],{"categoryNo":225,"categoryName":226,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":228,"categoryName":229,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":82,"categoryName":83,"taxonomyType":76,"taxonomyTypeDesc":77},[232,233,234,235,236,237,238,239,177,240,22,124,241,180,242,211,243,244,18,245],"react","component","refactoring","component refactoring","react component","react component refactoring","frontend","writing","safe","react components","modular","refactor","simplify","test-driven-development"]