AI coding agents are great at writing Prisma code that works and quietly terrible at writing Prisma code that scales. The most common thing I see in reviews: a findMany followed by a loop that does one more query per row.
// what the agent writes
const projects = await db.project.findMany();
for (const p of projects) {
p.owner = await db.user.findUnique({ where: { id: p.ownerId } });
}
I measured it on a tiny SQLite database with Prisma 7 and query logging on: 6 projects → 7 queries. The same data with a nested select → 2 queries. With 500 rows the loop becomes 501 queries; the nested read stays flat.
The fix isn't "remember to tell the agent every time". It's giving the agent a short, scoped rule that loads only when it's touching data-access files, plus a skill it can run when something is already slow.
1. A scoped Cursor rule (.cursor/rules/05-prisma-queries-n-plus-one.mdc)
The frontmatter is the important part. globs makes Cursor attach the rule only when a matching file is in context, so it doesn't eat your context window on UI work:
---
description: "Prisma query patterns, select vs include, pagination and N+1 avoidance. Use when writing or reviewing database reads in the data access layer."
globs: src/server/**/*.ts, server/**/*.ts, src/lib/db/**/*.ts
alwaysApply: false
---
The body is intentionally blunt. Agents follow "forbidden pattern → fix" better than prose:
## N+1: forbidden patterns
// BAD: 1 query + N queries
const projects = await db.project.findMany();
for (const p of projects) p.owner = await db.user.findUnique({ where: { id: p.ownerId } });
// BAD: same thing hidden in Promise.all(map(...)) or in a child Server Component per row
Fix with one of:
- Nested read: db.project.findMany({ select: { id: true, name: true, owner: { select: { id: true, name: true } } } })
- Batch: collect ids → db.user.findMany({ where: { id: { in: ids } } }) → map in memory
- Counts: _count: { select: { tasks: true } } instead of loading children to count them
- Aggregates: groupBy, aggregate, count instead of loading rows into JS
- Lists of Server Components that each fetch? Fetch once in the parent and pass data down.
It also covers the things agents skip unless told:
-
Always
selectthe fields you need (whole rows leak columns). Reuse selections withsatisfies Prisma.ProjectSelect+Prisma.ProjectGetPayloadso the type follows the query. -
Bounded lists only: every user-facing
findManyhastakeand a max page size; prefer cursor pagination with a deterministicorderBy(addidas a tiebreaker). -
Authorization in the query:
where: { id, ownerId: user.id }instead of fetching then checking. -
Atomic counters:
{ views: { increment: 1 } }, not read-modify-write. -
Raw SQL only via tagged templates (
$queryRaw), never$queryRawUnsafewith user input.
2. A Claude Code skill for when it's already slow (.claude/skills/debug-n-plus-one/SKILL.md)
Rules prevent; skills fix. Claude Code picks a skill by its description, so the description says when to use it:
---
name: debug-n-plus-one
description: Find and fix N+1 queries and slow Prisma reads in a Next.js app by enabling query logging, counting queries per request, locating the loop and rewriting it with nested select, batching, _count or groupBy. Use when a page or endpoint is slow, the logs show many similar queries, or the user mentions N+1.
---
The steps are the ones I'd do by hand:
- Measure: enable query logging in dev only and count queries for one request.
const db = new PrismaClient({
adapter,
log: process.env.PRISMA_LOG_QUERIES ? [{ emit: "event", level: "query" }] : [],
});
db.$on("query", (e) => console.log(`[prisma] ${e.duration}ms ${e.query}`));
The N+1 signature is the same SELECT ... WHERE "id" = $1 repeated per row.
-
Locate the loop:
awaitinsidefor/map,Promise.all(items.map(...db...)), child Server Components that each query, loading children just to count them. -
Fix with the smallest correct change (nested
select, batch within+Map,_count,groupBy, move fetching to the parent, add an index, addtake). - Verify: report query count and DB time before → after, and make sure the returned shape (and types) didn't change.
The "report before → after numbers" step matters: it forces the agent to prove the fix instead of claiming it.
Get the files
All three files (this rule, the skill, and a bonus Server Actions + zod rule) are free on Gumroad (enter $0): https://jrequena.gumroad.com/l/rlvxxqh. Drop .cursor/ and .claude/ into your repo root. In Claude Code, type / to see the skill.
Written for Prisma 7 (the prisma-client generator + driver adapters); the reference code in the pack was built and tested on Prisma 7.10. On Prisma 6, change the import path of the generated client; the patterns are the same.
Disclosure: I'm Juan, a full-stack dev from Chile. These files come from rule packs I sell. If you want the full set (15–16 scoped rules per stack for RSC/Server Actions or Vite + Hono, auth, testing, migrations, security, accessibility, plus 5 Claude Code skills, AGENTS.md and CLAUDE.md), there's a Next.js 15 + Prisma pack, a Vite + React + Hono + Prisma pack, and a bundle with both. The free files above are the real thing, not a teaser version.
Top comments (0)