- Prisma v7 Migration on Next.js 16 — Turbopack Fix Guide
Prisma v7 Migration on Next.js 16 — Turbopack Fix Guide
Step-by-step migration from Prisma v6 to v7 in Next.js 16, resolving Turbopack module errors and configuring…

⚡ Next.js Implementation Guides
In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
Related Posts:
I recently upgraded a production Next.js application to version 16, and with it came the need to migrate from Prisma v6 to v7. What should have been a straightforward package upgrade turned into several hours of debugging cryptic module resolution errors. The official Prisma v7 migration guide doesn't address the specific compatibility issues with Next.js 16's Turbopack bundler — and the fix isn't just one change, it's three.
After working through the issues and getting everything running smoothly, I wanted to document the exact process. This guide walks you through migrating from Prisma v6 to Prisma v7 (currently v7.4.2) in a Next.js 16 application using Turbopack, with proper PostgreSQL adapter configuration. By the end, you'll have a working Prisma v7 setup that's fully compatible with Turbopack's module bundling system.
TL;DR — 4 things to get Prisma v7 running on Next.js 16 + Turbopack:
- Keep
provider = "prisma-client-js"inschema.prisma(notprisma-client) - Add
serverExternalPackages: ['@prisma/client', 'pg']tonext.config.ts - Move all connection URLs from
schema.prismatoprisma.config.ts - Instantiate
PrismaClientwith an adapter — barenew PrismaClient()is invalid in v7
Understanding the Prisma v7 Changes
Prisma v7 introduces several breaking changes that fundamentally alter how the client is instantiated and configured. Here's a quick comparison of what changed:
| Prisma v6 | Prisma v7 | |
|---|---|---|
| Generator provider | prisma-client-js | prisma-client (new) |
| Connection config | url in schema.prisma | datasource.url in prisma.config.ts |
| Client instantiation | new PrismaClient() | new PrismaClient({ adapter }) |
| Env loading | Auto-loaded from .env | Requires import 'dotenv/config' |
| Query caching | Not available | Built-in (introduced in v7.4.0) |
The most significant change is the requirement for driver adapters. In previous versions, Prisma handled database connections internally through its query engine. Version 7 moves to an adapter-based architecture where you explicitly provide a database driver, making the main Prisma Client leaner and more flexible.
Upgrading the Prisma Packages
The first step is upgrading both the Prisma CLI and client packages to version 7. The CLI is a dev dependency used for migrations and client generation, while the client package is the runtime dependency your application uses to query the database.
pnpm add @prisma/client@7.4.2 pnpm add -D prisma@7.4.2
Along with Prisma itself, you'll need to install the PostgreSQL adapter and driver packages, as well as dotenv for environment variable management in the new config system.
pnpm add @prisma/adapter-pg@^7.0.0 pg@^8.11.x dotenv pnpm add -D @types/pg
These packages work together to provide the connection layer that Prisma v7 requires. The adapter translates between Prisma's query interface and the pg driver's native connection pooling.
Configuring the Prisma Schema
The schema file structure changes in Prisma v7. The datasource block no longer contains the database URL directly, as this configuration moves to the new prisma.config.ts file. This separation allows for more flexible configuration patterns and better alignment with TypeScript-based configuration systems.
Update your schema file to remove the URL configuration:
// File: prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } // Your models remain unchanged model User { id String @id @default(uuid()) email String @unique name String // ... rest of your schema } datasource db { provider = "postgresql" } // Your models remain unchanged model User { id String @id @default(uuid()) email String @unique name String // ... rest of your schema }
The key change here is removing the url and directUrl properties from the datasource block. Notice we're using prisma-client-js as the provider rather than the new prisma-client name mentioned in the official docs. This is intentional and critical for Turbopack compatibility, which I'll explain in detail later.
Remove url, directUrl, and shadowDatabaseUrl from the datasource block — not just url. Leaving any of them will trigger: error: the datasource property url is no longer supported in schema files
Creating the Prisma Configuration File
Prisma v7 introduces a new TypeScript configuration file that handles runtime settings including database URLs, migration paths, and other CLI behaviors. This file uses the defineConfig helper and supports type-safe environment variable access.
import 'dotenv/config' at the top of this file is mandatory. Prisma v7 no longer auto-loads .env files for the CLI. If you skip this, environment variables will be undefined when running migrations. (Exception: Bun auto-loads env vars, so Bun users can omit it.)
Create the configuration file at your project root:
// File: prisma.config.ts
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL_UNPOOLED'),
},
migrations: {
path: 'prisma/migrations',
},
});
This configuration tells Prisma where to find your schema file and where to generate migrations. The env() helper provides type-safe access to environment variables. I'm using DATABASE_URL_UNPOOLED here because my project uses connection pooling with a service like Neon or Supabase, where the unpooled URL is needed for migrations. If you're running a standard PostgreSQL instance, you can use env('DATABASE_URL') instead.
If you're running prisma generate in a Docker build stage that doesn't have a live database available, you can bypass the strict env() helper and use process.env.DATABASE_URL! to avoid errors at build time when the variable may not be set:
// For CI/Docker build stages without a live database
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: {
url: process.env.DATABASE_URL!,
},
migrations: {
path: 'prisma/migrations',
},
});
Updating the Prisma Client Instantiation
The way you create and export your Prisma Client changes significantly in version 7. Instead of directly instantiating PrismaClient, you now create a database adapter first, then pass it to the client constructor.
The way you create and export your Prisma Client changes significantly in version 7. Instead of directly instantiating PrismaClient, you now create a database adapter first, then pass it to the client constructor.
In Prisma v7, new PrismaClient() without arguments throws: PrismaClient needs to be constructed with a non-empty, valid PrismaClientOptions. The internal connection engine has been removed. You must pass { adapter } — there is no fallback.
// File: lib/db/prisma.ts
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
const { Pool } = pg;
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
pool: pg.Pool | undefined;
};
// Create connection pool for the adapter
const pool = globalForPrisma.pool ?? new Pool({
connectionString: process.env.DATABASE_URL,
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.pool = pool;
}
// Create Prisma adapter
const adapter = new PrismaPg(pool);
// Create Prisma Client with adapter
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
adapter,
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export default prisma;
This code creates a PostgreSQL connection pool using the pg driver, wraps it in the Prisma adapter, and passes that adapter to the PrismaClient constructor. The global caching pattern prevents Next.js hot reload from creating multiple client instances during development. Notice we're caching both the pool and the prisma client to the global object, ensuring connection reuse across hot reloads.
The adapter acts as a bridge between Prisma's query interface and the native PostgreSQL driver. When Prisma needs to execute a query, it translates it into the format expected by the pg driver, which then handles the actual database communication through the connection pool.
The Triple-Layered Turbopack Fix
Here's where things get tricky. The official Prisma v7 migration guide recommends changing your generator provider from prisma-client-js to prisma-client. However, this creates a module resolution error with Next.js 16's Turbopack bundler. The new prisma-client provider generates client code in an ESM-optimized structure outside of node_modules, and Turbopack's module hashing loses reference to the internal runtime during SSR — trapping you in a Cannot find module '.prisma/client/default' loop.
The fix requires three changes, not one. Using only the generator workaround without the next.config.ts changes will still fail in some SSR configurations.
Switching to prisma-client-js alone is often not enough. All three steps below are required for reliable Turbopack compatibility.
Step 1 — Keep prisma-client-js as the generator provider
Your schema's generator block should look exactly like this:
generator client { provider = "prisma-client-js" }
Do not add an output field, and do not use prisma-client as the provider.
Step 2 — Externalize Prisma packages in next.config.ts
Tell Next.js to treat these packages as server-external so Turbopack doesn't try to bundle them:
// File: next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['@prisma/client', 'pg'],
};
export default nextConfig;
Step 3 — Add a Turbopack resolveAlias
If you're still hitting module resolution errors after steps 1 and 2, add an explicit alias pointing Turbopack to the generated Prisma client:
// File: next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
serverExternalPackages: ['@prisma/client', 'pg'],
experimental: {
turbo: {
resolveAlias: {
'.prisma/client/default': './node_modules/.prisma/client/default.js',
},
},
},
};
export default nextConfig;
This is likely a temporary workaround that will be resolved as Turbopack matures and Prisma updates its generator to better support different bundler configurations.
Generating and Testing the Client
With your schema and configuration files updated, generate the Prisma client:
npx prisma generate
You should see output indicating the client was generated successfully. The output will show the path where the client was created, typically in your node_modules under the .pnpm/@prisma+client@7.x.x/node_modules/@prisma/client directory when using pnpm.
If you have existing migrations, apply them with:
npx prisma migrate deploy
For new migrations during development, use:
npx prisma migrate dev --name your_migration_name
The migration commands now read their configuration from prisma.config.ts, using the database URL you specified there. This means you can have different URLs for migrations versus runtime queries, which is useful when working with connection pooling services.
Start your development server and test that database queries work correctly:
pnpm dev
Navigate to any route that performs database operations. If you see the "Cannot find module" error at this point, double-check that your generator uses prisma-client-js and doesn't have an output path specified. That combination is what makes it work with Turbopack.
Why This Configuration Works
The reason prisma-client-js works while prisma-client doesn't comes down to module resolution paths and how different bundlers handle them. The newer prisma-client provider generates its output in an ESM-optimized structure outside of node_modules — which is great for modern bundlers but currently causes Turbopack to lose track of the generated client during the SSR phase. The older prisma-client-js provider generates the client in the traditional node_modules/.prisma/client location that Turbopack's resolution algorithm handles reliably.
The serverExternalPackages config tells Next.js not to bundle @prisma/client and pg at all — they're loaded at runtime from node_modules instead. This sidesteps the resolution conflict entirely. The resolveAlias is a further safety net that gives Turbopack an explicit path when its heuristic resolution fails.
Prisma v7's client is fully functional using the old provider name. The internal implementation is identical — it's purely a difference in output structure. By combining all three layers, you get all the performance improvements of v7 (including the query caching layer introduced in v7.4.0) while maintaining full compatibility with Next.js 16's bundler.
Troubleshooting Common Errors
Here are the most common errors developers hit during this migration and their exact fixes.
error: The datasource.url property is required in your Prisma config file when using prisma migrate deploy
This occurs during production deployments or Docker builds when the CLI can't resolve the connection string. Either prisma.config.ts wasn't copied into the build image, or the env() helper failed because the environment variable is undefined at that build stage.
Fix: In Docker environments, explicitly copy the config file in your Dockerfile:
COPY prisma.config.ts ./prisma.config.ts
If the error occurs during a prisma generate step that doesn't actually need a database connection, replace env('DATABASE_URL') with process.env.DATABASE_URL! in prisma.config.ts to bypass the strict env validation.
cannot find module 'prisma/config'
This almost always appears in monorepos or CI pipelines where Prisma is run via a global npx prisma install. The @prisma/config sub-module doesn't hoist correctly in that case.
Fix: Ensure prisma is installed as a local devDependency in the specific project workspace, not relied on globally:
pnpm add -D prisma@7.4.2
Then run npx prisma from the project directory with the local install in scope.
`PrismaClient` needs to be constructed with a non-empty, valid `PrismaClientOptions`
This happens when you call new PrismaClient() without passing a driver adapter. In v7, the internal connection engine was removed — there is no fallback.
Fix: Always instantiate with an adapter:
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
error: the datasource property url is no longer supported in schema files
This is a hard breaking change in v7. The CLI rejects any schema that defines connection variables locally.
Fix: Remove url, directUrl, and shadowDatabaseUrl from your schema.prisma datasource block entirely — not just url. Then define them all inside the datasource object of prisma.config.ts.
Verifying Your Migration
After completing the migration, verify that everything works correctly by testing your application's database operations. Check that queries execute successfully, that connections are properly pooled, and that there are no runtime errors in your server logs. Pay particular attention to server actions and API routes, as these are where Prisma client usage is most common.
You should also verify that your development workflow still functions as expected. Hot reload should work without creating connection pool exhaustion, migrations should apply cleanly, and the Prisma Studio should connect properly if you use it.
Summary
Migrating to Prisma v7 on Next.js 16 with Turbopack requires more than just a package upgrade. The key changes are: keeping prisma-client-js as the generator provider, externalizing @prisma/client and pg via serverExternalPackages, moving all connection URLs to prisma.config.ts, and instantiating PrismaClient with the @prisma/adapter-pg adapter. Together these four changes give you stable Turbopack compatibility while unlocking all of v7's improvements — faster queries, smaller bundles, and the query caching layer introduced in v7.4.0.
If you're running into other Next.js 16 compatibility issues, the same Turbopack-related module resolution patterns come up with other packages too — see the fix for the "Couldn't find next-intl config file" error for a similar debugging approach. If you're also migrating your CMS layer, the Payload CMS & Next.js 16 compatibility guide covers the parallel upgrade path.
If you run into issues during your migration or have questions about specific configuration scenarios, let me know in the comments.
Thanks, Matija
Frequently Asked Questions
Comments
No comments yet
Be the first to share your thoughts on this post!


