DEV Community

Juan Requena
Juan Requena

Posted on

Stop your AI agent from writing N+1 Prisma queries (free Cursor rule + Claude Code skill)

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 } });
}
Enter fullscreen mode Exit fullscreen mode

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
---
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

It also covers the things agents skip unless told:

  • Always select the fields you need (whole rows leak columns). Reuse selections with satisfies Prisma.ProjectSelect + Prisma.ProjectGetPayload so the type follows the query.
  • Bounded lists only: every user-facing findMany has take and a max page size; prefer cursor pagination with a deterministic orderBy (add id as 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 $queryRawUnsafe with 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.
---
Enter fullscreen mode Exit fullscreen mode

The steps are the ones I'd do by hand:

  1. 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}`));
Enter fullscreen mode Exit fullscreen mode

The N+1 signature is the same SELECT ... WHERE "id" = $1 repeated per row.

  1. Locate the loop: await inside for/map, Promise.all(items.map(...db...)), child Server Components that each query, loading children just to count them.
  2. Fix with the smallest correct change (nested select, batch with in + Map, _count, groupBy, move fetching to the parent, add an index, add take).
  3. 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)