Fix "Vercel build failed: Module not found" in AI-generated Next.js apps
The app runs fine on your laptop. You push to GitHub, Vercel starts building, and a minute later you get:
Failed to compile. ./app/dashboard/page.tsx Module not found: Can't resolve '@/components/ui/data-table' > Build failed because of webpack errors Error: Command "npm run build" exited with 1
It's one of the most-searched Next.js build errors, and code from AI tools runs into it all the time. Cursor, v0, Bolt and Claude write imports by following common conventions, which may not match how your project is actually laid out. The fix is usually one line. The trick is working out which of five causes you have. The build log already tells you most of it.
Read the log line properly
Open the failed deployment in Vercel, go to Build Logs, and find the first Module not found. It tells you two things:
- The importing file, for example
./app/dashboard/page.tsx. This is where the bad import lives. - The specifier, the text in quotes. If it starts with
.,@/or~/, it's a file in your repo. Anything else, like'recharts'or'@radix-ui/react-dialog', is an npm package.
Then reproduce it locally the same way Vercel builds: from a clean install.
rm -rf node_modules .next npm ci # or: pnpm install --frozen-lockfile npm run build
If the clean local build passes but Vercel fails, the cause is almost always #1 or #2 below. If it fails locally too, you can fix it before pushing again.
1. File name casing (works on Mac, fails on Vercel)
macOS and Windows treat DataTable.tsx and data-table.tsx as the same file. Vercel builds on Linux, where they are two different files. AI agents often create a component as UserCard.tsx and import it as ./user-card, or rename a file in a way git never records.
# list what git actually has (that's what Vercel sees) git ls-files | grep -i datatable
If git shows components/ui/DataTable.tsx but the import says data-table, fix one side to match. A case-only rename on Mac needs git mv, because otherwise git ignores it:
git mv components/ui/DataTable.tsx components/ui/data-table.tsx git commit -m "fix: lowercase data-table filename"
2. The file was never committed
The agent created lib/utils.ts on your machine, but it's untracked, or .gitignore excludes it. It builds locally and doesn't exist on Vercel. Check:
git status --short # ?? means untracked git check-ignore -v lib/utils.ts # prints the rule if it's ignored
Watch for .gitignore lines like lib/ (common in Python templates), /components, or *.local.ts. Projects uploaded to GitHub through the web uploader often lose folders or get flattened, so every file ends up at the repo root. If dozens of imports fail at once, compare the repo's tree with your local one before you touch any code.
3. The @/ alias doesn't point where the AI thinks
@/components/... is just an alias defined in tsconfig.json (or jsconfig.json). If the project has no src/ folder but the alias maps to ./src/*, every @/ import breaks. The reverse breaks too.
// tsconfig.json: project WITH a src/ folder
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } }
// project WITHOUT src/ (app/ and components/ at root)
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./*"] } } }
Also check for shadcn's components.json. It has its own aliases block, and a mismatch there makes npx shadcn add put components in the wrong folder. It's a classic way to get "it imports @/components/ui/button but the file is in src/components/ui/button.tsx".
A monorepo or a Vite project needs the alias in the bundler as well. Vite needs it in vite.config.ts, under resolve.alias, or through the vite-tsconfig-paths plugin.
4. The package isn't in package.json
If the specifier is a package name, the AI used a library it never installed, or installed it globally, or added it to the wrong package.json in a monorepo.
npm ls recharts # "empty" means it's not a dependency npm install recharts # adds it to package.json AND the lockfile git add package.json package-lock.json
Lockfiles cause their own variety of this error:
- Two lockfiles (
package-lock.jsonpluspnpm-lock.yamlorbun.lockb). Vercel picks a package manager based on the lockfile, which may not be the one you used. Delete the extras and keep one. - A lockfile out of sync with package.json.
npm cifails, or installs something different. Regenerate it with a normal install and commit the result. - An agent "cleaned up dependencies" and bumped
nextto a canary or a new major version, so internal imports likenext/navigationornext/fontstop resolving. Checkgit diff HEAD~5 -- package.jsonand pin exact versions you know work.
5. Server-only modules imported into client code
This one looks like: Module not found: Can't resolve 'fs', 'net', 'tls', 'dns' or 'child_process'. Nothing is actually missing. A Node.js-only library ended up in a bundle that runs in the browser. Usual causes:
- A file with
"use client"imports something that pulls in a database driver (pg,mysql2,ioredis),nodemailer, or the Supabase service-role client. - A shared
lib/file exports both a React helper and a database helper, so importing one brings in the other.
// lib/db.ts: make accidental client imports fail loudly at build
import "server-only";
import { Pool } from "pg";
export const db = new Pool({ connectionString: process.env.DATABASE_URL });
Move the data access into a Server Component, a Route Handler (app/api/.../route.ts) or a Server Action, and have client components call it. Don't silence the error with webpack: { resolve: { fallback: { fs: false } } }. That only hides the problem, and you risk shipping server code, or secrets, to the browser.
Why AI tools keep causing this
A model writing an import is predicting what the path probably is. When your project differs from the most common layout (a src/ folder or not, PascalCase or kebab-case, shadcn in ui/ or not), it guesses wrong. Then, when the build fails, the usual AI "fix" is to create a second copy of the component at the guessed path, and now you have two drifting versions. A few habits stop this:
- Before asking for a new component, reference your real structure (in Cursor,
@-mention an existing component andtsconfig.json). - Add a project rule: "All imports use
@/, which maps to./src. Files are kebab-case. Never create a duplicate file to satisfy an import, fix the import instead." - Run
npm run build(not justdev) before every push. A CI step that runsnext buildon pull requests catches all five causes before Vercel does.
The 60-second triage
- Specifier starts with
.or@/? Rungit ls-files | grep -i <name>. If it's missing, see #2. If it's there with different casing, see #1. If it's there but undersrc/versus root, see #3. - Specifier is a package? Run
npm ls <pkg>. If it's empty, see #4. - Specifier is
fs,net,tlsordns? See #5.
A worked example
Suppose Vercel fails with:
./src/app/settings/page.tsx Module not found: Can't resolve '@/components/SettingsForm'
On your Mac, src/components/settings-form.tsx exists and the page imports it as @/components/SettingsForm. Locally it works because the filesystem is case-insensitive. On Vercel:
$ git ls-files | grep -i settings src/components/settings-form.tsx
Git has the kebab-case name. Fix the import, not the file:
// before
import { SettingsForm } from "@/components/SettingsForm";
// after
import { SettingsForm } from "@/components/settings-form";
Or rename with git mv if you prefer PascalCase everywhere. Either way, one side has to match. Commit, push, and the next Vercel build should get past this line. If the next error is a different missing module, repeat the triage. It is normal for AI-generated apps to have several of these stacked; fixing them one at a time is faster than asking the agent to "fix the build" in one go.
Prevent it in CI
Add a GitHub Actions job that runs the same commands Vercel will. Failures then show up on the pull request, not after you merge:
# .github/workflows/build.yml
name: build
on: [pull_request, push]
jobs:
next:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: npm }
- run: npm ci
- run: npm run build
Using npm ci (or pnpm install --frozen-lockfile) matters. It refuses to invent a lockfile, so a drift that would confuse Vercel fails in CI first. Pin Node to the same major version your host uses.
FAQ
Why does my Next.js app build locally but fail on Vercel with Module not found?
Most often the import casing does not match the file name. macOS and Windows ignore case, while Vercel builds on case-sensitive Linux. The second most common cause is a file that exists locally but was never committed to git.
How do I fix "Can't resolve '@/components/...'"?
Check that the paths alias in tsconfig.json points to the right folder: ./src/* if you have a src directory, ./* if you do not. Then confirm the file exists at that exact path with git ls-files.
Why does Next.js say Module not found: Can't resolve 'fs'?
A Node.js-only module was imported into code that runs in the browser, often through a "use client" component or a shared lib file. Move that code to a Server Component, Route Handler or Server Action, and add import "server-only" to the module.
Should I let the AI agent update my dependencies to fix build errors?
Review any change to package.json or the lockfile yourself. Agents sometimes bump frameworks to canary or new major versions, which causes new Module not found errors. Pin known-good versions and use npm ci in CI.