Prisma 5 Prisma 6: The Breaking Changes I Hit in My Real Schema and How I Fixed Them Without Breaking Production

typescript dev.to

Prisma 5 → Prisma 6: The Breaking Changes I Hit in My Real Schema and How I Fixed Them Without Breaking Production

The correct approach to migrating from Prisma 5 to Prisma 6 without breaking anything is don't run the upgrade on a Friday. I know that sounds obvious. But that's not actually the point — the real point is this: Prisma 6 has changes that TypeScript's compiler is not going to yell at you about. They'll pass through silently, and you'll find out at runtime — or worse, from a query result that looks correct but isn't.

My thesis: Prisma 6 is a genuine improvement in ergonomics and performance, but there are three behavior changes that require manual attention before you upgrade. They're not bugs — they're deliberate decisions by the Prisma team that change how relational queries, the generated client, and transactions behave. If you don't know about them upfront, they'll find you.

What follows is my analysis of those three changes, with representative code and the checklist I built so I don't have to repeat the experience.


Why Prisma 6 Matters (and What the Official Announcement Actually Says)

The official announcement — "What's new in Prisma 6" — has three main pillars:

  1. Better performance — rewritten internals, more efficient query engine.
  2. More flexibility — improved support for multiple providers and client configuration.
  3. Type-safe SQL — the new prisma.$queryRawTyped API with real type inference.

All of that is real and welcome. What the announcement doesn't emphasize enough — and what costs you time when you upgrade without reading the full migration guide — are the behaviors that changed silently.

I'm going to cover the three that hit hardest in a Next.js 16 + Server Actions + PostgreSQL stack.


Change 1: selectRelationCount Is No Longer Opt-In — How You Count Relations Changed

In Prisma 5, if you wanted to count relations (say, how many posts a user has) inside a select, you had to enable the selectRelationCount preview feature in the schema:

// schema.prisma — Prisma 5
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["selectRelationCount"]
}
Enter fullscreen mode Exit fullscreen mode

In Prisma 6, selectRelationCount went GA and the preview flag was removed. If you leave it in the schema, the Prisma CLI throws a warning — or an outright error depending on the exact version. The functionality still works, but the API changed subtly in how it integrates with include vs select.

// ✅ Prisma 5 — worked with the preview feature active
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    _count: {
      select: { posts: true }
    }
  }
})

// ✅ Prisma 6 — same syntax, but without the flag in the schema
// If the flag is still there, the CLI emits a warning on generate
const users = await prisma.user.findMany({
  select: {
    id: true,
    name: true,
    _count: {
      select: { posts: true }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Concrete action: grep all previewFeatures in your schema and verify which ones went GA in v6. The official guide lists them. Remove them before running prisma generate.


Change 2: The Behavior of undefined in Relational Queries Changed

This is the one that hurts the most because there's no compile-time error. In Prisma 5, passing undefined as a value in a where was ignored — the filter simply wasn't applied. In Prisma 6, that behavior was standardized more strictly: in some cases undefined is still ignored, but in others — especially inside nested selects with optional relations — the behavior differs depending on whether the field is nullable in the schema or not.

// ⚠️ Dangerous pattern in the Prisma 5 → 6 transition
async function getPosts(categoryFilter?: string) {
  return await prisma.post.findMany({
    where: {
      // In Prisma 5: if categoryFilter is undefined, this where was ignored
      // In Prisma 6: behavior depends on the field's type in the schema
      // If 'category' is an optional field (String?), it may behave differently
      category: categoryFilter,
    },
    include: {
      author: true
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

The fix is explicit and more defensive:

// ✅ Safe pattern for both Prisma 5 and 6
async function getPosts(categoryFilter?: string) {
  return await prisma.post.findMany({
    where: {
      // Build the where conditionally — don't depend on undefined's behavior
      ...(categoryFilter !== undefined && { category: categoryFilter }),
    },
    include: {
      author: true
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

The uncomfortable part about this change: the TypeScript types in the generated client don't change. String | undefined is still a valid type in the where. The compiler tells you nothing. You have to find it manually or with integration tests.

My take here: building where objects conditionally isn't a workaround — it's the correct practice in any version of Prisma. If your codebase has a lot of places where you pass optional variables directly into where, this is the moment to clean them up.


Change 3: The Generated Client Was Reorganized and Direct Type Imports Can Break

Prisma 6 reorganized the structure of the generated client. If anywhere in your codebase you're importing types directly from the .prisma/client folder or from internal package paths (something that shouldn't be done but shows up in old tutorials), those imports can break silently or with cryptic errors.

// ❌ Fragile pattern — importing from internal paths of the generated client
// This might have worked in Prisma 5 but it's a private API, not public
import { Prisma } from '@prisma/client/edge'

// ✅ Always import from the public entry point
import { Prisma, PrismaClient } from '@prisma/client'
Enter fullscreen mode Exit fullscreen mode

The most common case in Next.js 16 with Server Actions: using the edge client (@prisma/client/edge) for middleware or routes running in the Edge Runtime. In Prisma 6, the edge client configuration was unified and the way you instantiate it changed. The official docs have the updated details, but the error you'll see if you don't update is generic — something like "cannot find module" or "type is not assignable" that doesn't point directly at the problem.

// ✅ Prisma 6 with Next.js 16 — single client instance
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    // log only in development — don't expose query logs in production
    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  })

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Enter fullscreen mode Exit fullscreen mode

This pattern didn't change between v5 and v6, but if you had it misconfigured (multiple instances, broken singleton), the upgrade is the moment to fix it.


Common Migration Mistakes — The Gotchas That Keep Showing Up

Gotcha 1: running prisma db push without reading the full output.

Prisma 6 can generate slightly different migrations for the same schema if there are fields with types that changed internally (like some DateTime types with precision). Review the migration diff before applying it.

Gotcha 2: assuming prisma migrate dev and prisma migrate deploy behave the same way.

migrate dev can do additional things (like resetting the DB on conflicts). In any environment that resembles production, always use migrate deploy and check state with prisma migrate status first.

# Check migration state before upgrading
npx prisma migrate status

# Generate the client after updating the version
npx prisma generate

# Review schema differences without applying anything
npx prisma migrate diff \
  --from-schema-datasource prisma/schema.prisma \
  --to-schema-datamodel prisma/schema.prisma \
  --script
Enter fullscreen mode Exit fullscreen mode

Gotcha 3: not updating devDependencies alongside @prisma/client.

prisma (the CLI) and @prisma/client need to be on the same major version. If you update one and not the other, you'll get client generation errors that are hard to diagnose.

# Always update both at the same time
npm install prisma@6 @prisma/client@6

# Or with pnpm
pnpm add prisma@6 @prisma/client@6
Enter fullscreen mode Exit fullscreen mode

Gotcha 4: transaction behavior with $transaction and timeouts.

Prisma 6 adjusted the default timeouts for interactive transactions. If you have transactions running slow operations, the default timeout may be different. Verify and set it explicitly:

// ✅ Explicit timeout — don't depend on the default
await prisma.$transaction(
  async (tx) => {
    // transaction operations
  },
  {
    maxWait: 5000,  // ms — max time waiting to acquire the transaction
    timeout: 10000  // ms — max execution time
  }
)
Enter fullscreen mode Exit fullscreen mode

Prisma 5 → 6 Migration Checklist

This is the order I follow to upgrade without surprises. It's not the only path, but it covers the edge cases that come up most often:

Before the upgrade:

  • [ ] Review all previewFeatures in the schema — verify which ones went GA in v6 and remove the corresponding flags
  • [ ] Audit all where clauses that receive optional variables — replace the field: variable | undefined pattern with explicit conditional construction
  • [ ] Search for imports from internal @prisma/client paths — centralize on the public entry point
  • [ ] Verify explicit timeouts on all interactive $transaction calls
  • [ ] Run prisma migrate status and make sure there are no pending migrations before upgrading

During the upgrade:

  • [ ] Update prisma and @prisma/client to the same major version simultaneously
  • [ ] Run prisma generate and review the full output — not just that it finishes without error
  • [ ] Run the integration test suite (if you have one) pointing at a staging DB, not production
  • [ ] If you use Next.js 16 with Edge Runtime, verify the edge client configuration per the Prisma 6 docs

After the upgrade:

  • [ ] Monitor query logs in the first few hours — look for slower queries or unexpected results in optional relations
  • [ ] Verify that the PrismaClient singleton is still working correctly in Next.js's lifecycle (hot reload in dev, single instance in prod)

FAQ — Prisma 6 Migration Breaking Changes

Is Prisma 6 compatible with Prisma 5 without changes?

Not completely. There are breaking changes documented in the official migration guide. Most are manageable, but they require manual review — especially in schemas with previewFeatures, relational queries with optional values, and edge client usage. This isn't a patch upgrade; take it seriously.

Does the Prisma schema (schema.prisma) change between v5 and v6?

The schema format didn't change dramatically, but there are previewFeatures flags that need to be removed because they went GA. If you leave them in, the CLI may emit warnings or errors depending on the exact version. Check the full list in the official announcement.

Will my queries with include and optional relations work the same?

Probably yes for simple cases. The risk is in queries that pass undefined conditionally to optional fields in where. If you build filters explicitly (without depending on undefined's behavior), the risk is low.

Does Prisma 6 work with Next.js 16 App Router and Server Actions?

Yes. The Next.js 16 + Server Actions + Prisma 6 + PostgreSQL stack works well. What needs attention is the client instance (global singleton) and the Edge Runtime configuration if you use it. The Prisma patterns with Server Actions I covered in the previous post on Server Actions and Prisma are still valid — just verify the client entry point.

Can I upgrade directly in production?

My recommendation is no. The safest flow is: upgrade branch → staging with a DB similar to production → integration test suite → deploy during low-traffic hours. The upgrade itself isn't risky if you follow the checklist, but the prior validation is what saves you from surprises.

Does $queryRawTyped replace $queryRaw?

It doesn't replace it, it complements it. $queryRawTyped is the new API for type-safe SQL with type inference — a genuine improvement for complex SQL queries that the ORM can't express well. $queryRaw still works. If you want to explore the new API, the official announcement has the examples; if you already use query logging with PostgreSQL to debug heavy queries, $queryRawTyped will be a natural ally.


The Upgrade Is Worth It, But You Have to Earn It

Prisma 6 is a real step forward — better performance, faster client generation, and type-safe SQL are concrete improvements that you feel in projects with complex schemas. I'm not questioning that.

What I am saying is that there are three behaviors that won't scream at you in the compiler: cleaning up previewFeatures, handling undefined in conditional where objects, and imports from the generated client. Ignore them, and you find out at runtime.

The truly uncomfortable part is that none of the three are Prisma bugs — they're reasonable decisions by the team that prioritize correct behavior over silent compatibility. But if you don't read the full migration guide before running npm install prisma@6, you're the one paying the cost.

My practical recommendation: before upgrading, run a grep through the codebase for previewFeatures, for field: variable patterns in where objects, and for imports from internal @prisma/client paths. If all three come back clean, the upgrade will be smooth. If something shows up, you know before you start.

If you're on the path of query hardening and logging, the post on Prisma query logging and PostgreSQL has useful context for the monitoring side post-upgrade. And if the project uses TypeScript strict mode, the strictNullChecks and noUncheckedIndexedAccess options will make exactly the undefined patterns I described here more visible.


Original source:


This article was originally published on juanchi.dev

Source: dev.to

arrow_back Back to Tutorials