Startup blocked on pnpm dev#
Prerequisites#
Before diving into the debugging process and applying the solutions, ensure that your environment meets the following requirements:
- Next.js: 13.4+ (App Router and Edge Middleware support)
- Prisma: 5.x+ (native Edge Runtime support)
- Node.js: 18+ (development and execution environment)
- pnpm: 8.x+ (or an equivalent manager like npm or yarn)
The error surfaced at the moment of starting the development environment. The Next.js server did not even complete the bootstrap process: compilation aborted immediately with the following terminal output:
⨯ ./node_modules/.../@prisma/client/default.js:2:6
Module not found: Can't resolve '.prisma/client/default'
1 | module.exports = {
> 2 | ...require('.prisma/client/default'),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 | }
Import trace:
Edge Middleware:
./node_modules/.../@prisma/client/default.js
./src/lib/db-client.ts
./src/lib/auth.ts
./middleware.ts
Two distinct problems are nested in this single output, and treating them separately is what defines the difference between a superficial fix and a real architectural resolution.
Problem 1 - the "ghost" client: missing prisma-generated artifact#
Problem anatomy#
The @prisma/client package that exists in node_modules after a pnpm install is deliberately an empty shell. The @prisma/client directory contains only the generic client codebase. The actual module, which knows your tables, models, relationships, TypeScript types, and database URL: it is not downloaded from the internet; it must be generated locally by the Prisma CLI based on the project's schema.prisma file.
The generated artifact is deposited in the .prisma/client/ directory inside node_modules. This directory is not versioned in Git (it is in .gitignore by default) and is not created by pnpm install. It only exists after explicitly running the generator.
Why does this error appear? Common scenarios:
- Clean repository clone followed only by
pnpm install - Cache clearing with
pnpm store pruneor manualnode_modulesremoval - Prisma version update without regenerating the client
- CI/CD environment missing the
prisma generatestep
Checking the current state of the generated artifact:
# If this directory does not exist or is empty, the error will occur
ls node_modules/.prisma/client/
# Expected in healthy state:
# default.js default.d.ts edge.js edge.d.ts index.js index.d.ts ...
# schema.prisma libquery_engine-*.so.node (or .dll on Windows)
Layer 1 resolution: prisma generate#
pnpm prisma generate
Expected success output:
Prisma schema loaded from prisma/schema.prisma
Environment variables loaded from .env
✔ Generated Prisma Client (v5.x.x) to ./node_modules/@prisma/client in 1.2s
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client):
import { PrismaClient } from '@prisma/client'
What prisma generate does exactly:
- Reads the
schema.prismaand validates the syntax - Introspects the types and relationships defined in the models
- Compiles the Query Engine binary specific to the current OS/architecture (
.so.nodeon Linux,.dllon Windows,.dylibon macOS) - Generates TypeScript types for all models (
PrismaClient,User,Post, etc.) - Deposits everything in
node_modules/.prisma/client/
Integration in package.json to prevent recurrence:
{
"scripts": {
"dev": "prisma generate && next dev",
"build": "prisma generate && next build",
"postinstall": "prisma generate"
}
}
The postinstall script ensures prisma generate runs automatically after any pnpm install, including in CI/CD environments and Vercel deployments.
After prisma generate, the restarted pnpm dev compiled successfully. But the log analysis revealed the work was far from over.
Problem 2 - the architectural trap: prisma client in the edge runtime#
Reading the import trace with forensic attention#
The Import trace field in the original error is the most critical, and most ignored, point of the entire analysis:
Import trace:
Edge Middleware:
./node_modules/.../@prisma/client/default.js ← Prisma Client being imported
./src/lib/db-client.ts ← Prisma wrapper
./src/lib/auth.ts ← Authentication module
./middleware.ts ← NEXT.JS MIDDLEWARE FILE
The Edge Middleware: prefix is the fire alarm. It indicates the Next.js compiler is attempting to include the Prisma Client in the Edge Runtime bundle, a fundamental execution environment incompatibility.
What is next.js edge runtime and why prisma doesn't work there#
The Edge Runtime is a highly latency-optimized JavaScript execution environment used by Next.js middleware.ts (and optionally by Route Handlers with export const runtime = 'edge'). It is based on V8 Isolates, the same architecture used by Cloudflare Workers, and operates under intentionally severe restrictions:
| Capability | Node.js Runtime | Edge Runtime |
|---|---|---|
fs (File System) | ✅ Available | ❌ Absent |
child_process | ✅ Available | ❌ Absent |
net (TCP sockets) | ✅ Available | ❌ Absent |
Native .node modules | ✅ Available | ❌ Absent |
| Web APIs (fetch, crypto) | ✅ Available | ✅ Available |
| Max bundle size | ~50 MB | ~1-4 MB |
| Cold start | ~100-500ms | ~1-5ms |
The standard Prisma Client is fundamentally incompatible with the Edge Runtime because:
- Depends on the native Query Engine binary - a compiled
.so.nodeor.dllfile that useschild_processfor IPC communication with the Prisma process - Uses TCP sockets via
netto connect to the database - Accesses the filesystem to read the schema and database SSL certificate
- Exceeds the bundle size limit of the Edge environment
The runtime error that would occur (after fixing only prisma generate):
Error: PrismaClient is unable to run in this browser environment, or has been bundled
for the browser (running in `EdgeRuntime`).
In case you want to use Prisma Client in an Edge environment, you can use the Prisma
Accelerate extension: https://pris.ly/d/accelerate.
If this is unexpected, please open an issue: https://pris.ly/prisma-client-env
Or more directly in deployment environments:
⨯ TypeError: Cannot read properties of undefined (reading 'connect')
⨯ Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.
This error would only manifest in production or when testing the middleware in staging: a latent bug invisible during local development if the application starts successfully.
Why was the middleware importing prisma?#
The most common pattern that leads to this situation:
// middleware.ts (PROBLEMATIC)
import { NextRequest, NextResponse } from 'next/server'
import { validateSession } from '@/lib/auth' // ← Imports auth.ts
// auth.ts imports db-client.ts
// db-client.ts imports PrismaClient
// PrismaClient → .prisma/client/default.js → INCOMPATIBLE WITH EDGE
export async function middleware(request: NextRequest) {
const session = await validateSession(request) // Making DB query in middleware!
if (!session) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
The problem is the transitive import chain: the middleware does not import Prisma directly, but imports auth.ts which imports db-client.ts which imports PrismaClient. The Next.js compiler detects the entire dependency tree and includes it in the bundle, not just what is directly called.
The 3 architectural resolution routes#
Route 1 - JWT decoupling in the middleware (the adopted choice)#
The strategy: Remove all database calls from the middleware entirely. Instead of validating the session by querying the database, validate only the cryptographic integrity of the authentication token.
To do this, the middleware uses the jose library, which is optimized for Web APIs and has zero native Node.js dependencies.
Installing the jose library:
# Via pnpm
pnpm add jose
# OR via npm
npm install jose
# OR via yarn
yarn add jose
# Verify the installation
pnpm list jose
Robust and edge-optimized middleware#
Below is the recommended implementation of middleware.ts, covering detailed error handling, secure cookie and header manipulation, and redirection scenarios:
// middleware.ts (CORRECT - Zero Node.js Dependencies)
import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
// In the Edge Runtime, process.env is accessible but with limitations:
// 1. Only variables prefixed with NEXT_PUBLIC_ are exposed to the client
// 2. Sensitive variables (like JWT secrets) must be accessed exclusively on the server/middleware
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
if (!token) {
// Redirect 302 (Temporary - default)
// Redirects the user to the login screen if there is no session cookie
return NextResponse.redirect(new URL('/login', request.url))
}
try {
// Pure cryptographic verification - without executing database queries in the Edge
const { payload } = await jwtVerify(token, JWT_SECRET)
// Validate if the JWT payload contains the minimum mandatory claims
if (!payload.sub || !payload.role) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Inject payload data into headers for reading in Server Components
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', payload.sub as string)
requestHeaders.set('x-user-role', payload.role as string)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
// Example of cookie manipulation in the Edge Runtime (Sliding Session)
// Updates the cookie expiration on each valid request
response.cookies.set('auth-token', token, {
httpOnly: true, // Protection against XSS attacks (prevents reading cookies via script)
secure: process.env.NODE_ENV === 'production', // Requires HTTPS in production
sameSite: 'lax', // Mitigation of CSRF attacks
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
})
return response
} catch (error: any) {
// Granular error handling based on jose library error codes
console.error('JWT validation error in Edge:', error.code || error.message)
// ERR_JWT_EXPIRED: Expired token
if (error.code === 'ERR_JWT_EXPIRED') {
const response = NextResponse.redirect(new URL('/login?expired=true', request.url))
// Clear the expired cookie to prevent redirection loops
response.cookies.delete('auth-token')
return response
}
// ERR_JWS_SIGNATURE_VERIFICATION_FAILED or other errors (Invalid/corrupted token)
const response = NextResponse.redirect(new URL('/login?invalid=true', request.url))
response.cookies.delete('auth-token')
return response
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|login).*)']
}
Redirection scenarios in the middleware#
When working with redirects in Next.js Middleware, use status codes matching your intent:
- Status 302 (Found): The default temporary redirect. Ideal for routing unauthenticated users to
/login. - Status 307 (Temporary Redirect): A strict temporary redirect that guarantees the original HTTP method is preserved (e.g., a POST request remains a POST request at the target URL).
NextResponse.redirect(url, { status: 307 }). - Status 301 (Moved Permanently): A permanent redirect. Used primarily for path migrations to preserve SEO authority.
- External Redirection: Useful for delegating authentication to an external provider (SSO/OAuth):
NextResponse.redirect('https://auth.provider.com/login').
Environment variables in the edge runtime#
In the Next.js Edge Runtime:
- On the server side (Middleware / Server Components): Sensitive environment variables (like
JWT_SECRET) are accessible viaprocess.env.JWT_SECRET, as long as they are defined in the VPS configuration or the.envfile. - On the client side (Client Components): Only variables prefixed with
NEXT_PUBLIC_are bundled into the client build. Secret variables without this prefix returnundefinedin the browser to prevent sensitive data exposure.
Organizing multiple middlewares (pipeline)#
Since Next.js supports only a single global middleware.ts file in the project root, the best architectural practice to manage multiple middlewares (e.g., authentication, logging, and rate limiting) is using a pipeline wrapper pattern:
// middleware.ts - Main Middleware (Orchestrator)
import { NextRequest, NextResponse } from 'next/server'
import { authMiddleware } from './middleware/auth'
import { rateLimitMiddleware } from './middleware/rate-limit'
export async function middleware(request: NextRequest) {
// 1. Execute rate limiting
const rateLimitResponse = await rateLimitMiddleware(request)
if (rateLimitResponse) return rateLimitResponse
// 2. Execute authentication validation
const authResponse = await authMiddleware(request)
if (authResponse) return authResponse
return NextResponse.next()
}
Recommended configurations in next.config.js#
Useful security headers and configurations for API routes running in Next.js:
// next.config.js
module.exports = {
// Global security headers against clickjacking, MIME sniffing, and referer leaks
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }
],
},
];
},
};
// src/lib/auth.ts (REFACTORED - heavy logic moved to Server Components)
import { prisma } from '@/lib/db-client'
// This function MUST ONLY be called in Server Components or Route Handlers (Node.js Runtime)
export async function getFullUserSession(userId: string) {
return await prisma.user.findUnique({
where: { id: userId },
include: { permissions: true, organization: true }
})
}
// app/dashboard/layout.tsx (Server Component - Node.js Runtime)
import { headers } from 'next/headers'
import { getFullUserSession } from '@/lib/auth'
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const headersList = headers()
const userId = headersList.get('x-user-id')
// Node.js Runtime: Prisma runs here
const user = await getFullUserSession(userId!)
if (!user?.permissions?.canAccessDashboard) {
redirect('/unauthorized')
}
return <>{children}</>
}
Trade-offs of this approach:
- ✅ Zero additional latency in middleware (cryptographic verification is ~0.1ms)
- ✅ Works on any edge (Vercel, Cloudflare, AWS Lambda@Edge)
- ⚠️ The JWT may be signed but the user may have been deactivated in the database - full validation happens in Server Components
Route 2 - prisma accelerate or HTTP serverless drivers#
When to use: If the business rule requires rejecting the request at the middleware level with database data (e.g., checking if an IP is banned, if a slug exists, if a feature flag is active for the user).
Prisma Accelerate is Prisma's managed service that exposes the database via HTTP instead of TCP, making it Edge-compatible:
// prisma/schema.prisma - add Accelerate extension
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
pnpm add @prisma/extension-accelerate
// src/lib/edge-db-client.ts (For EXCLUSIVE use in middleware/edge)
import { PrismaClient } from '@prisma/client/edge'
import { withAccelerate } from '@prisma/extension-accelerate'
// Prisma Accelerate URL - different from the standard DATABASE_URL
const prismaEdge = new PrismaClient({
datasourceUrl: process.env.DATABASE_URL_ACCELERATE, // prisma://...
}).$extends(withAccelerate())
export { prismaEdge }
Alternative with Neon (serverless PostgreSQL via HTTP):
// For Neon Database - native HTTP driver
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
// Use in middleware - direct HTTP query without TCP
const result = await sql`SELECT id FROM banned_ips WHERE ip = ${clientIP}`
Route 3 - reconfiguring middleware runtime to Node.js#
Not possible. Next.js middleware.ts is mandatorily executed in the Edge Runtime. There is no valid export const runtime = 'nodejs' configuration for middleware.ts files - this is an intentional design limitation of the framework to guarantee ultra-fast cold starts.
If you attempt:
// middleware.ts - DOES NOT WORK
export const runtime = 'nodejs' // ← Silently ignored or generates a build error
Next.js ignores or rejects this declaration specifically in the context of global middleware.
Diagnostic checklist: import trace as a forensic tool#
Whenever a compilation error appears in the Next.js context, the Import trace is the starting point:
# Force verbose compiler output to see the full trace
NEXT_TELEMETRY_DISABLED=1 pnpm dev 2>&1 | grep -A 20 "Import trace"
Warning signals in the Import trace:
| Prefix | Meaning | Action |
|---|---|---|
Edge Middleware: | Middleware/Edge bundle | Remove Node.js dependencies |
Server Component: | Server Component (OK for Node.js) | No action needed |
Client Component: | Browser bundle | Remove server-side dependencies |
Route Handler: | API Route (OK for Node.js) | No action needed |
The chain reads from bottom to top: middleware.ts → auth.ts → db-client.ts → @prisma/client. Each link in the chain is a decoupling candidate.
Decision flow: resolving prisma client in next.js#
Runtime capability comparison: Node.js vs edge#
| Capability / API | Node.js Runtime | Edge Runtime | Recommended Alternative on Edge |
|---|---|---|---|
fs (File System) | ✅ Available | ❌ Absent | Use Cloud Storage / S3 |
child_process | ✅ Available | ❌ Absent | Use Microservices / External API |
net (TCP Sockets) | ✅ Available | ❌ Absent | HTTP queries via fetch |
Native .node modules | ✅ Available | ❌ Absent | WASM compiled equivalents |
| Web APIs (fetch, crypto) | ✅ Available | ✅ Available | - |
| Bundle size limit | ~50MB | ~1MB - 4MB | Optimize imports / Tree shaking |
| Startup time (Cold Start) | ~100-500ms | ~1-5ms | - |
Testing and validating the middleware#
Before pushing corrections to production, validate the middleware behavior using the following steps.
Testing the middleware via terminal (cURL)#
Execute these cURL commands in your local terminal to verify the redirect and cookie behaviors:
# 1. Test access to a protected route (/dashboard) without providing cookies
curl -I http://localhost:3000/dashboard
# Expected output: Status 302 Found with a Location header pointing to /login
# 2. Test access with an invalid or modified token cookie
curl -I -H "Cookie: auth-token=invalid_token" http://localhost:3000/dashboard
# Expected output: Status 302 Found redirecting to /login (and clearing the cookie)
# 3. Test access with a valid and signed token
# Note: You can generate a mock JWT containing { "sub": "123", "role": "admin" }
curl -I -H "Cookie: auth-token=YOUR_VALID_JWT" http://localhost:3000/dashboard
# Expected output: Status 200 OK
Post-build validation checklist#
Run these commands in your repository root to confirm the project is consistent:
# 1. Verify that the Prisma Client artifacts are generated
ls -la node_modules/.prisma/client/
# Must display default.js, edge.js, index.js, etc.
# 2. Start the development server to ensure it bootstraps without panics
pnpm dev 2>&1 | head -20
# 3. Monitor Next.js server logs for any compilation or run-time exceptions
tail -n 50 .next/server/server-app.log 2>/dev/null
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments