DEV Community

Cover image for Fix Next.js Build Error Module Not Found After Deploy
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on • Originally published at iloveblogs.blog

Fix Next.js Build Error Module Not Found After Deploy

Fix Next.js Build Error Module Not Found After Deploy

"Module not found" errors that only appear after deployment are among the most confusing Next.js issues. Your app works perfectly in development, builds successfully locally, but fails in production with cryptic module errors. This comprehensive guide covers every cause and provides battle-tested solutions.

This article is part of our comprehensive Deploying Next.js + Supabase to Production guide.

Why Module Errors Only Happen in Production

Development and production environments differ in critical ways:

  1. Case Sensitivity - Production servers (Linux) are case-sensitive, Windows/Mac aren't
  2. Path Resolution - Different module resolution algorithms
  3. Build Optimization - Tree-shaking removes "unused" imports
  4. Environment Variables - Missing or incorrect env vars
  5. Dependencies - devDependencies not installed in production

Common Error Messages

## Error 1: Cannot find module
Error: Cannot find module '@/components/Header'

## Error 2: Module not found (Webpack)
Module not found: Can't resolve './components/header'

## Error 3: Dynamic import failure
Error: Cannot find module './pages/dashboard'

## Error 4: Package not found
Module not found: Can't resolve 'some-package'
Enter fullscreen mode Exit fullscreen mode

Solution 1: Fix Case Sensitivity Issues

The #1 cause of production-only module errors:

Identify Case Mismatches

// BAD: Import doesn't match actual filename
// File: components/Header.tsx
import { Header } from '@/components/header'  // ❌ lowercase 'h'

// File: lib/utils.ts
import { formatDate } from './Utils'  // ❌ Capital 'U'

// GOOD: Exact case match
// File: components/Header.tsx
import { Header } from '@/components/Header'  // ✅ Capital 'H'

// File: lib/utils.ts
import { formatDate } from './utils'  // ✅ lowercase 'u'
Enter fullscreen mode Exit fullscreen mode

Find All Case Mismatches

## Install case-sensitive-paths-webpack-plugin
npm install --save-dev case-sensitive-paths-webpack-plugin

## Add to next.config.mjs
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'

const nextConfig = {
  webpack: (config, { isServer }) => {
    config.plugins.push(new CaseSensitivePathsPlugin())
    return config
  },
}

export default nextConfig
Enter fullscreen mode Exit fullscreen mode

Automated Fix Script

// scripts/fix-case-sensitivity.js
const fs = require('fs')
const path = require('path')
const glob = require('glob')

// Find all TypeScript/JavaScript files
const files = glob.sync('**/*.{ts,tsx,js,jsx}', {
  ignore: ['node_modules/**', '.next/**'],
})

files.forEach(file => {
  let content = fs.readFileSync(file, 'utf8')
  let modified = false

  // Check each import statement
  const importRegex = /from ['"](.+)['"]/g
  let match

  while ((match = importRegex.exec(content)) !== null) {
    const importPath = match[1]

    // Skip node_modules and absolute paths
    if (!importPath.startsWith('.') && !importPath.startsWith('@/')) {
      continue
    }

    // Resolve actual file path
    const resolvedPath = path.resolve(path.dirname(file), importPath)

    // Check if file exists with different case
    if (fs.existsSync(resolvedPath)) {
      const actualPath = fs.realpathSync(resolvedPath)
      if (actualPath !== resolvedPath) {
        console.log(`Case mismatch in ${file}:`)
        console.log(`  Import: ${importPath}`)
        console.log(`  Actual: ${actualPath}`)
        modified = true
      }
    }
  }

  if (modified) {
    console.log(`Fixed: ${file}`)
  }
})
Enter fullscreen mode Exit fullscreen mode

Solution 2: Fix Path Alias Configuration

Incorrect tsconfig.json or jsconfig.json paths:

Verify Path Aliases

// tsconfig.json or jsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],           //  Correct
      "@/components/*": ["./src/components/*"],  //  Specific paths
      "@/lib/*": ["./src/lib/*"],
      "~/*": ["./*"]                //  Root alias
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Common Path Alias Mistakes

//  WRONG: Missing baseUrl
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]  // Won't work without baseUrl
    }
  }
}

//  WRONG: Incorrect path format
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]  // Missing ./
    }
  }
}

//  CORRECT
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Update Next.js Config

// next.config.mjs
const nextConfig = {
  // Ensure webpack resolves aliases correctly
  webpack: (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, 'src'),
    }
    return config
  },
}
Enter fullscreen mode Exit fullscreen mode

Solution 3: Fix Dynamic Imports

Dynamic imports can fail in production:

Incorrect Dynamic Import

// ❌ BAD: Variable path
const pageName = 'dashboard'
const Page = dynamic(() => import(`./pages/${pageName}`))

// ❌ BAD: Computed path
const getComponent = (name) => dynamic(() => import(`@/components/${name}`))
Enter fullscreen mode Exit fullscreen mode

Correct Dynamic Import

// ✅ GOOD: Static path with dynamic loading
const DashboardPage = dynamic(() => import('./pages/dashboard'))

// ✅ GOOD: Explicit imports with conditional rendering
const components = {
  dashboard: dynamic(() => import('@/components/Dashboard')),
  profile: dynamic(() => import('@/components/Profile')),
  settings: dynamic(() => import('@/components/Settings')),
}

function MyComponent({ type }) {
  const Component = components[type]
  return <Component />
}

// ✅ GOOD: Use webpack magic comments
const DynamicComponent = dynamic(() => 
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackMode: "lazy" */
    './pages/dashboard'
  )
)
Enter fullscreen mode Exit fullscreen mode

Solution 4: Fix Missing Dependencies

Dependencies in wrong section of package.json:

Check Dependency Location

// package.json

//  WRONG: Build-time dependency in devDependencies
{
  "devDependencies": {
    "@supabase/supabase-js": "^2.38.0",  // Used in production!
    "next": "15.0.0"  // Should be in dependencies!
  }
}

//  CORRECT: Production deps in dependencies
{
  "dependencies": {
    "@supabase/supabase-js": "^2.38.0",
    "next": "15.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "typescript": "^5.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Move Dependencies

## Move package from devDependencies to dependencies
npm install --save some-package
npm uninstall --save-dev some-package

## Or manually edit package.json and run:
npm install
Enter fullscreen mode Exit fullscreen mode

Solution 5: Fix Environment-Specific Imports

Some imports only work in specific environments:

Server-Only Imports

// ❌ BAD: Server-only import in client component
'use client'

import fs from 'fs'  // ❌ fs doesn't exist in browser

export function MyComponent() {
  // ...
}

// ✅ GOOD: Use server actions or API routes
'use client'

export function MyComponent() {
  async function handleAction() {
    const response = await fetch('/api/read-file')
    const data = await response.json()
  }

  return <button onClick={handleAction}>Read File</button>
}

// app/api/read-file/route.ts
import fs from 'fs'  // ✅ OK in API route

export async function GET() {
  const data = fs.readFileSync('file.txt', 'utf8')
  return Response.json({ data })
}
Enter fullscreen mode Exit fullscreen mode

Solution 6: Fix Vercel-Specific Issues

Vercel has specific requirements:

Output File Tracing

// next.config.mjs
const nextConfig = {
  // Ensure all dependencies are traced
  experimental: {
    outputFileTracingIncludes: {
      '/api/**/*': ['./node_modules/**/*.wasm', './node_modules/**/*.node'],
    },
  },

  // Exclude unnecessary files
  experimental: {
    outputFileTracingExcludes: {
      '*': [
        'node_modules/@swc/core-linux-x64-gnu',
        'node_modules/@swc/core-linux-x64-musl',
        'node_modules/@esbuild/linux-x64',
      ],
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Vercel Build Command

// package.json
{
  "scripts": {
    "build": "next build",
    "vercel-build": "next build"  // Vercel uses this if present
  }
}
Enter fullscreen mode Exit fullscreen mode

Solution 7: Debug Build Locally

Test production build locally:

## Build for production
npm run build

## Start production server
npm run start

## Or use Vercel CLI
npm install -g vercel
vercel build
vercel dev --prod
Enter fullscreen mode Exit fullscreen mode

Enable Build Debugging

// next.config.mjs
const nextConfig = {
  // Show detailed build info
  productionBrowserSourceMaps: true,

  // Log webpack build
  webpack: (config, { dev, isServer }) => {
    if (!dev) {
      console.log('Production build for:', isServer ? 'server' : 'client')
    }
    return config
  },
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

  • Mistake #1: Inconsistent file naming - Use consistent casing across all files

  • Mistake #2: Wrong dependency section - Runtime deps must be in dependencies

  • Mistake #3: Dynamic imports with variables - Use static imports or explicit mapping

  • Mistake #4: Not testing production builds - Always test with npm run build && npm run start

  • Mistake #5: Ignoring TypeScript errors - Fix all TS errors before deploying

FAQ

Why does it work locally but not in production?

Local development (Windows/Mac) is case-insensitive, production (Linux) is case-sensitive. Also, dev mode doesn't optimize/tree-shake like production builds.

How do I find which module is missing?

Check the full error stack trace in your deployment logs. The error shows the exact import path that failed.

Can I make production case-insensitive?

No, but you can add case-sensitive-paths-webpack-plugin to catch issues during development.

Why do dynamic imports fail?

Webpack needs to know possible import paths at build time. Variable paths prevent this. Use static imports or explicit mapping.

Should I use relative or absolute imports?

Both work, but absolute imports (@/) are cleaner and easier to refactor. Just ensure tsconfig.json is configured correctly.

Advanced Debugging Strategies

Using Build Analysis Tools

## Analyze what's included in your build
npm install --save-dev @next/bundle-analyzer

## Add to next.config.mjs
import bundleAnalyzer from '@next/bundle-analyzer'

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})

export default withBundleAnalyzer(nextConfig)

## Run analysis
ANALYZE=true npm run build
Enter fullscreen mode Exit fullscreen mode

Debugging Import Resolution

// Create a debug script to trace imports
// scripts/debug-imports.js
const fs = require('fs')
const path = require('path')
const glob = require('glob')

const files = glob.sync('**/*.{ts,tsx,js,jsx}', {
  ignore: ['node_modules/**', '.next/**'],
})

files.forEach(file => {
  const content = fs.readFileSync(file, 'utf8')
  const importRegex = /from ['"](.+)['"]/g
  let match

  while ((match = importRegex.exec(content)) !== null) {
    const importPath = match[1]

    // Check if file exists
    const resolvedPath = path.resolve(path.dirname(file), importPath)
    if (!fs.existsSync(resolvedPath)) {
      console.log(`❌ Missing: ${importPath} in ${file}`)
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Production Build Simulation

## Build and test locally before deploying
npm run build

## Start production server
npm run start

## Test critical routes
curl http://localhost:3000/
curl http://localhost:3000/api/health

## Check for errors in console
Enter fullscreen mode Exit fullscreen mode

Environment-Specific Module Issues

Handling Platform-Specific Modules

// ❌ BAD: Platform-specific import in client code
import fs from 'fs'  // Only works on server

// ✅ GOOD: Use dynamic imports with ssr: false
import dynamic from 'next/dynamic'

const FileReader = dynamic(
  () => import('@/components/FileReader'),
  { ssr: false }
)

// ✅ GOOD: Use server actions
'use server'

export async function readFile(path: string) {
  const fs = await import('fs')
  return fs.readFileSync(path, 'utf8')
}
Enter fullscreen mode Exit fullscreen mode

Conditional Imports Based on Environment

// lib/platform-specific.ts
let platformModule: any

if (typeof window === 'undefined') {
  // Server-side only
  platformModule = require('fs')
} else {
  // Client-side only
  platformModule = require('browser-storage')
}

export default platformModule
Enter fullscreen mode Exit fullscreen mode

Monorepo-Specific Issues

Handling Monorepo Module Resolution

// tsconfig.json in monorepo root
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@app/*": ["apps/app/src/*"],
      "@api/*": ["apps/api/src/*"],
      "@shared/*": ["packages/shared/src/*"],
      "@ui/*": ["packages/ui/src/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Monorepo Build Configuration

// next.config.mjs in monorepo app
const nextConfig = {
  webpack: (config, { isServer }) => {
    // Ensure monorepo packages are resolved
    config.resolve.alias = {
      ...config.resolve.alias,
      '@shared': path.resolve(__dirname, '../../packages/shared/src'),
      '@ui': path.resolve(__dirname, '../../packages/ui/src'),
    }
    return config
  },
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimization for Large Projects

Lazy Load Heavy Dependencies

// ❌ BAD: Import everything upfront
import * as lodash from 'lodash'

// ✅ GOOD: Lazy load only what you need
import { debounce } from 'lodash'

// ✅ BETTER: Use dynamic imports
const { debounce } = await import('lodash')
Enter fullscreen mode Exit fullscreen mode

Tree-Shaking Configuration

// next.config.mjs
const nextConfig = {
  // Ensure tree-shaking works
  webpack: (config) => {
    config.optimization.usedExports = true
    config.optimization.sideEffects = true
    return config
  },

  // Mark packages as side-effect free
  modularizeImports: {
    'lodash': {
      transform: 'lodash/{{member}}',
    },
    '@mui/material': {
      transform: '@mui/material/{{member}}',
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Testing Module Resolution

Create a Module Resolution Test

// __tests__/module-resolution.test.ts
import { describe, it, expect } from 'vitest'

describe('Module Resolution', () => {
  it('should resolve all path aliases', async () => {
    const modules = [
      '@/components/Button',
      '@/lib/utils',
      '@/types/index',
      '@/hooks/useAuth',
    ]

    for (const modulePath of modules) {
      const module = await import(modulePath)
      expect(module).toBeDefined()
    }
  })

  it('should not have circular dependencies', async () => {
    // Use madge to check
    const madge = require('madge')
    const result = await madge('src')
    expect(result.circular()).toHaveLength(0)
  })
})
Enter fullscreen mode Exit fullscreen mode

Deployment Platform-Specific Fixes

Vercel-Specific Configuration

// next.config.mjs for Vercel
const nextConfig = {
  // Ensure all files are traced
  experimental: {
    outputFileTracingIncludes: {
      '/api/**/*': ['./node_modules/**/*.wasm'],
    },
  },

  // Exclude unnecessary files
  experimental: {
    outputFileTracingExcludes: {
      '*': [
        'node_modules/@swc/core-linux-x64-gnu',
        'node_modules/@swc/core-linux-x64-musl',
      ],
    },
  },
}

export default nextConfig
Enter fullscreen mode Exit fullscreen mode

AWS Lambda/Netlify Configuration

// next.config.mjs for serverless
const nextConfig = {
  // Optimize for serverless
  experimental: {
    isrMemoryCacheSize: 0, // Disable ISR cache for serverless
  },

  // Ensure dependencies are bundled
  webpack: (config) => {
    config.externals = []
    return config
  },
}

export default nextConfig
Enter fullscreen mode Exit fullscreen mode

Related Articles

Conclusion

Module not found errors in production are usually caused by case sensitivity mismatches, incorrect path aliases, or dependencies in the wrong package.json section. The fastest fix is ensuring all imports match exact file names (including case) and moving runtime dependencies to the dependencies section.

Always test production builds locally with npm run build && npm run start before deploying. Use case-sensitive-paths-webpack-plugin during development to catch issues early.

For Vercel deployments, check output file tracing configuration and ensure all necessary files are included in the build. Monitor deployment logs carefully to identify the exact module causing issues.


Originally published at https://iloveblogs.blog

Top comments (0)