nextjs-client-server-boundary-module-errors
Next.js 16 Client-Server Boundary Module Resolution Errors
Problem
Next.js 16 build fails with errors like:
Module not found: Can't resolve 'dns'
Module not found: Can't resolve 'fs'
Module not found: Can't resolve 'net'
Module not found: Can't resolve 'tls'
The error trace shows:
Import trace:
./node_modules/.pnpm/pg@8.16.3/...
./packages/database/index.ts [Client Component Browser]
./apps/web/lib/utils/some-util.ts [Client Component Browser]
./apps/web/components/SomeClientComponent.tsx [Client Component Browser]
This happens when a client component ("use client") imports constants or utilities from a file that transitively imports Node.js-only modules (like Prisma, database clients, or server-side libraries).
Context / Trigger Conditions
When this occurs:
- Building Next.js 16 app with
next buildorturbo build - Client component imports constants/types from a utility file
- That utility file also contains server-side functions that import database/Node.js modules
- The error mentions Node.js built-in modules:
dns,fs,net,tls,child_process, etc.
Why it happens: Next.js 16 enforces strict client/server boundaries. When you import ANY export from a file, the entire file gets bundled. If that file imports server-only modules (even if you're only using constants), those server imports try to resolve in the browser bundle and fail.
Common scenarios:
- Constants defined alongside database utility functions
- Type definitions in files that also import ORM clients
- Enums/config objects in server action files
- Shared validators in files with server-side imports
Solution
Step 1: Identify the Import Chain
Look at the error's import trace to find where the client component is pulling in server code:
./apps/web/lib/utils/preferences.ts [Client Component Browser]
./apps/web/components/NotificationPreferencesClient.tsx [Client Component Browser]
This tells you preferences.ts is being bundled for the browser because a client component imports from it.
Step 2: Examine the Problematic File
Read the file that's causing issues (e.g., lib/utils/preferences.ts):
// ❌ PROBLEM: Server imports in same file as constants
import { db, PreferenceCategory } from "database"; // Server-only!
export const EMAIL_NOTIFICATION_KEYS = {
BOOKING: "email.notification.booking",
// ...
};
export async function getEmailPreference(db: any, userId: string) {
// Server-only function using db
}
The client component only needs EMAIL_NOTIFICATION_KEYS, but importing it pulls in the entire file including the database import.
Step 3: Extract Constants to Separate File
Create a new constants-only file with NO server-side imports:
New file: lib/constants/email-preferences.ts
/**
* Email Preference Constants
*
* Shared constants for email notification preferences.
* This file has no server-side dependencies and can be safely imported in client components.
*/
export const EMAIL_NOTIFICATION_KEYS = {
BOOKING: "email.notification.booking",
PAYMENT: "email.notification.payment",
MESSAGING: "email.notification.messaging",
VERIFICATION: "email.notification.verification",
} as const;
export type EmailNotificationKey =
(typeof EMAIL_NOTIFICATION_KEYS)[keyof typeof EMAIL_NOTIFICATION_KEYS];
export const MESSAGING_COOLDOWN_MS = 60 * 60 * 1000;
Step 4: Update Original Utility File
Import and re-export from the new constants file for backward compatibility:
Modified: lib/utils/preferences.ts
import { db, PreferenceCategory } from "database"; // Server imports OK here
import {
EMAIL_NOTIFICATION_KEYS,
MESSAGING_COOLDOWN_MS,
type EmailNotificationKey,
} from "@/lib/constants/email-preferences"; // Import from constants file
// Re-export for backward compatibility
export { EMAIL_NOTIFICATION_KEYS, MESSAGING_COOLDOWN_MS };
export type { EmailNotificationKey };
// Server-only functions remain here
export async function getEmailPreference(db: any, userId: string) {
// ...
}
Step 5: Update Client Component Imports
Change client component to import directly from constants file:
"use client";
// ❌ Before: Imports from file with server dependencies
import { EMAIL_NOTIFICATION_KEYS } from "@/lib/utils/preferences";
// ✅ After: Imports from constants-only file
import { EMAIL_NOTIFICATION_KEYS } from "@/lib/constants/email-preferences";
Step 6: Verify Fix
turbo build # or next build
Build should succeed. The client component now imports from a file with no server dependencies.
Verification
- Build succeeds:
turbo buildornext buildcompletes without module resolution errors - Client bundle clean: No Node.js modules in browser bundle
- Route renders: The page using the client component renders correctly
- No runtime errors: No "Module not found" errors in browser console
Check the build output for your route:
Route (app)
├ ◐ /[locale]/settings/notifications ✓ Should show without errors
Alternative Solutions
Option 1: Turbopack resolveAlias (Workaround, Not Recommended)
You can configure Turbopack to load empty modules for Node.js built-ins:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
fs: { browser: './empty.ts' },
dns: { browser: './empty.ts' },
net: { browser: './empty.ts' },
tls: { browser: './empty.ts' },
},
},
}
export default nextConfig
Warning: This is a workaround that masks the problem. The recommended approach is to refactor your code to respect client/server boundaries.
Option 2: Duplicate Constants (Not Recommended)
Copying constants to both files works but creates maintenance burden:
// lib/utils/preferences.ts (server)
export const EMAIL_NOTIFICATION_KEYS = { /* ... */ };
// lib/constants/email-preferences.ts (client)
export const EMAIL_NOTIFICATION_KEYS = { /* ... */ }; // Duplicate!
Better: Extract once and import from the shared location.
Best Practices
1. Organize by Server/Client Capability
lib/
├── constants/ # Client-safe constants, types, enums (no server imports)
│ ├── email-preferences.ts
│ ├── routes.ts
│ └── validation-messages.ts
├── utils/ # Server-only utilities
│ ├── preferences.ts # Can import database
│ └── auth.ts # Can import server modules
└── shared/ # Truly universal utilities (no server or client-specific APIs)
└── formatters.ts # Pure functions only
2. Mark Constants Files Clearly
Add a comment banner to constants-only files:
/**
* Client-Safe Constants
*
* This file has no server-side dependencies and can be safely imported
* in both server components and client components.
*
* DO NOT add imports from: database, fs, path, crypto, or any Node.js modules.
*/
3. Use Separate Barrel Exports
If you have many constants, create a barrel export:
// lib/constants/index.ts
export * from './email-preferences';
export * from './routes';
export * from './validation';
// Client components import from barrel
import { EMAIL_NOTIFICATION_KEYS } from '@/lib/constants';
4. Validate Imports with ESLint
Consider using eslint-plugin-no-server-import-in-page or similar rules to catch these issues during development.
Notes
- This is specific to Next.js 16: Earlier versions had looser boundaries
- Applies to all client components: Any file with
"use client"directive - Transitive imports matter: Even if your file doesn't directly import Node.js modules, if it imports from a file that does, you'll hit this issue
- Server Components are safe: Server Components can import from files with Node.js modules without issues
- Type imports are usually safe:
import type { ... }statements are typically fine because they're erased at runtime, but mixed imports (import { type X, Y }) will still pull in the runtime code
Common Mistakes
❌ Mistake 1: Adding "use client" to the utility file
"use client"; // ❌ Doesn't fix the problem, makes it worse
import { db } from "database";
❌ Mistake 2: Using dynamic imports for constants
// ❌ Overly complex, constants should be static imports
const keys = await import('@/lib/utils/preferences').then(m => m.EMAIL_NOTIFICATION_KEYS);
❌ Mistake 3: Ignoring the error with webpack fallbacks
// ❌ Masks the problem, doesn't fix architecture
webpack: { resolve: { fallback: { fs: false } } }
✅ Correct: Extract constants to separate file with no server imports
Example: Complete Refactor
Before (broken):
lib/utils/preferences.ts <-- Has db imports
- EMAIL_NOTIFICATION_KEYS <-- Constants mixed with server code
- getEmailPreference() <-- Server function
components/NotificationClient.tsx <-- "use client"
- imports EMAIL_NOTIFICATION_KEYS <-- ❌ Pulls in db imports
After (fixed):
lib/constants/email-preferences.ts <-- NO server imports
- EMAIL_NOTIFICATION_KEYS <-- ✅ Pure constants
lib/utils/preferences.ts <-- Has db imports
- imports from constants file <-- ✅ Uses constants, doesn't define them
- re-exports for backward compat <-- ✅ Existing code still works
- getEmailPreference() <-- Server function
components/NotificationClient.tsx <-- "use client"
- imports from constants file <-- ✅ No server dependencies