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:
- Better performance — rewritten internals, more efficient query engine.
- More flexibility — improved support for multiple providers and client configuration.
- Type-safe SQL — the new
prisma.$queryRawTypedAPI 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"]
}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 }
}
}
})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
}
})
}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
}
})
}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'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 = prismaThis 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 \
--scriptGotcha 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@6Gotcha 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
}
)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
previewFeaturesin the schema — verify which ones went GA in v6 and remove the corresponding flags - Audit all
whereclauses that receive optional variables — replace thefield: variable | undefinedpattern with explicit conditional construction - Search for imports from internal
@prisma/clientpaths — centralize on the public entry point - Verify explicit timeouts on all interactive
$transactioncalls - Run
prisma migrate statusand make sure there are no pending migrations before upgrading
During the upgrade:
- Update
prismaand@prisma/clientto the same major version simultaneously - Run
prisma generateand 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
PrismaClientsingleton 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:
- Prisma — What's new in Prisma 6: https://www.prisma.io/blog/prisma-6-better-performance-more-flexibility-and-type-safe-sql
Related Articles
tsgo: what changes in the TypeScript compiler rewritten in Go and what it means for real projects
tsgo is real and the performance jump is verifiable — but the beta has documented limits that most posts quietly ignore. Here's the concrete criteria for deciding whether to explore it today or wait for stable.
React 19 use() hook and Suspense: when it replaces useEffect and when it throws you into a worse loop
React 19's use() hook promises to replace useEffect for data fetching. That promise is partially true. There are two patterns with Suspense and error boundaries where the behavior isn't what you expect and the cycle gets messier. I'll tell you exactly when to migrate and when not to.
Spring Boot Actuator: What to Expose, What to Hide, and What to Check Before Adding Endpoints
Actuator isn't the problem. The mistake is adding it to a project with no clear exposure policy. Here I break down which endpoints to enable, which to block, and what decisions to make before the first deploy.
Comments (0)
What do you think of this?
Drop your comment in 10 seconds.
We only use your login to show your name and avatar. No spam.