tsconfig paths in Next.js 16 App Router: when they help and when they silently break the build
Adding a path alias to tsconfig.json has the same energy as pinning a shortcut to your desktop: feels like a quality-of-life win right up until the day that shortcut points to nothing and there's no sign anywhere telling you why.
With tsconfig paths in Next.js 16 App Router, that's exactly the trap. tsc accepts it, the editor doesn't complain, the dev server spins up clean — and then the production build explodes silently, or worse: it finishes but with modules that didn't resolve the way you expected. And the log doesn't say "the alias is the problem." It says something cryptic about a module that doesn't exist, or a circular import that appeared out of nowhere.
My thesis before we get into it: tsconfig paths are not a universal tool. They're a type-mapping and editor tool that can integrate with the bundler, but only if you understand what each piece of the pipeline actually resolves. The real question isn't "use them or don't" — it's "understand who resolves what before you add an alias."
Why tsconfig paths and Next.js 16 aren't as straightforward as they look
Before talking about breakage, it's worth understanding what paths in tsconfig.json actually does.
Per the official TypeScript documentation, paths is an instruction for the type checker — not for the runtime, not for the bundler. TypeScript uses this mapping to know how to resolve types when it encounters an import like @/components/Button. What you do with that information afterward — running it in Node, bundling it with webpack or Turbopack, executing it in a worker — is some other tool's responsibility.
Next.js, for its part, documents path alias support and integrates it into its build pipeline. App Router with webpack or Turbopack reads the tsconfig.json and translates those aliases into the bundler's module resolver. That works — under certain conditions.
The problem shows up when those conditions aren't met. In a monorepo with pnpm workspaces, those conditions are more fragile than the documentation implies.
The 3 breakage cases that keep showing up
These are the most documented and reproducible failure patterns when working with tsconfig paths in Next.js 16 App Router inside a monorepo. These aren't hypotheticals — they're scenarios you can reproduce:
Case 1: tsc passes, the bundler can't find the module
The scenario: you configure a @ui/* alias pointing to an internal workspace package. The type checker doesn't complain. Neither does the dev server. You run next build and get:
Module not found: Can't resolve '@ui/button'
Why? Because in a pnpm workspace, the Next.js resolver (webpack or Turbopack) needs the package to be correctly linked in node_modules and the alias in tsconfig.json to be consistent with that physical path. If paths points to ../../packages/ui/src but the bundler expects to resolve from node_modules/@ui/button, you have a silent divergence.
The config that causes the problem:
// tsconfig.json — broken version
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
// TypeScript accepts this, but the bundler doesn't see the same thing
"@ui/*": ["../../packages/ui/src/*"]
}
}
}The fix: let the package manager resolve the package as a declared dependency, and use the alias only for the app's internal path:
// tsconfig.json — version that survives the build
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
// Local alias for the app, not for workspace packages
"@/*": ["./src/*"]
}
}
}For workspace packages, the dependency in package.json + the pnpm link is enough. No extra alias needed.
Case 2: Server Components don't propagate paths correctly
This one is the most subtle. In the App Router, Server Components run in a different Node.js context than the client. If a tsconfig.json alias resolves fine on the client but the target module imports something incompatible with the server environment (say, it uses browser APIs or has side effects that assume window), the build can fail during the server phase with an import error that looks like a resolution problem but is actually a compatibility problem.
The typical symptom:
Error: Cannot find module '@/lib/analytics'
at Function.Module._resolveFilename
Where @/lib/analytics exists and TypeScript doesn't complain. The real issue is that the module imports something incompatible with the server runtime, and Next.js doesn't always give you the full stack trace.
How to diagnose it: temporarily add "use client" to the failing component. If the error disappears, the problem isn't the alias — it's the target module's compatibility with the server runtime.
// diagnose-server-component.tsx
// Step 1: add this directive to isolate the source of the error
"use client"
// If the build passes with this directive and fails without it,
// the alias resolves fine — the problem is the target module
import { analytics } from "@/lib/analytics"Case 3: misconfigured baseUrl silently breaks absolute resolution
This shows up when you configure paths without a coherent baseUrl. The TypeScript documentation is clear: paths resolves relative to baseUrl. If baseUrl isn't defined or points to the wrong directory, your aliases are silent garbage.
The classic monorepo pattern for this: you copy a tsconfig.json from a single-repo project where baseUrl is "." (project root) and drop it into a package that has its own root. That "." now points somewhere else entirely.
// tsconfig.json for an internal package — broken
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
// baseUrl not overridden, inherits "." from the base
// which in the base context pointed to the monorepo root
// here it points to the package — base aliases are now useless
"paths": {
"@/*": ["./src/*"] // This may or may not be correct depending on context
}
}
}The simple rule: always declare baseUrl explicitly in every tsconfig.json that uses paths. Don't trust inheritance for this field.
What the official docs say and what they don't
The Next.js documentation on path aliases shows the happy path: a single-repo project with @/* pointing to ./src/*. Works perfectly in that scenario.
What the documentation doesn't explicitly cover:
- How the Next.js resolver interacts with aliases pointing outside the app directory (toward workspace packages)
- What happens when Turbopack and webpack resolve the same alias differently (this shifts between versions)
- How to debug when the build error doesn't mention the alias but rather the resulting module
The TypeScript documentation on paths is precise but doesn't mention bundlers. It's a type checker spec, not a runtime spec. Reading it through that lens changes how you interpret errors.
The uncomfortable truth: there's a documentation gap between "TypeScript accepts the alias" and "the production build accepts it too." That gap is where the three cases above live.
Decision checklist: when to configure paths and when not to
Before adding a new alias, run it through this filter:
Use tsconfig paths if:
- The alias points to a directory inside the same app (e.g.,
./src/components) baseUrlis declared explicitly in the same file- You can verify that
next buildpasses without the dev server running
Avoid tsconfig paths if:
- The alias points to a workspace package — let pnpm/npm resolve it as a dependency
- You're inheriting a
tsconfig.jsonwithout checking whatbaseUrlit brings along - The target module mixes browser and server imports
Check this before adding an alias:
# Verify the build passes cold, no cache
rm -rf .next
pnpm build
# If you use Turbopack in dev, also verify with webpack in build
# because they can resolve differently in early Next.js 16 versionsRed flag: if the build error mentions a module that physically exists but says it can't find it, an alias is involved. The clue is in the module path shown in the error — if it's different from the actual physical path, you have a resolution divergence.
For projects where TypeScript strict mode is active (which should be the norm in 2026), misconfigured aliases combined with noUncheckedIndexedAccess or moduleResolution: bundler can produce type errors that look like business logic bugs but are actually module resolution failures.
What you can't conclude without your own experiment
Before closing, clear limits:
- I can't claim these cases reproduce in every Next.js 16 configuration. The behavior can vary depending on the exact Next.js version, whether you're using Turbopack or webpack, and your TypeScript version.
- You can't assume that if the dev server doesn't fail, the production build won't either. They are different pipelines.
- It's not officially documented how Turbopack resolves aliases pointing outside the app directory in a monorepo. If you're using Turbopack in development and webpack in production (which was the default in Next.js 14-15), results can diverge.
To validate in your own environment: the reproducible experiment is rm -rf .next && pnpm build without the dev server. If it passes there, the alias is stable.
FAQ: common questions about tsconfig paths in Next.js
Does Next.js 16 automatically read the paths from tsconfig.json?
Yes, Next.js reads tsconfig.json and configures the webpack (or Turbopack) resolver with those aliases. But "reading" doesn't mean "resolving identically to TypeScript." The type checker and the bundler are different tools; Next.js bridges them, but with limitations in monorepo scenarios.
Is there a difference between baseUrl alone and baseUrl + paths?
Yes, and it matters. With just baseUrl, you can import from components/Button without a relative ./. Adding paths creates a named alias like @/components/Button. The second requires the first to work correctly — paths is relative to baseUrl.
Why doesn't the dev server fail but next build does?
Because the dev server uses an incremental compiler that tolerates more ambiguity. The production build does a full analysis of the module graph and is stricter about resolution. An alias the dev server "guesses correctly" can break in build.
Does Turbopack resolve paths the same way as webpack?
Not necessarily, especially in early Next.js 16 versions and for paths pointing outside the app directory. If you use --turbopack in dev, always verify the production build (which uses webpack by default) separately.
How do I know if an alias is causing the build error or if it's something else? Temporarily replace the alias with the relative path in the failing file. If the error disappears, the alias is the problem. If it persists, the cause is in the target module, not the mapping.
In a pnpm monorepo, is it worth using paths for workspace packages?
It's not the most robust approach. The most stable pattern is declaring the package as a dependency in package.json (e.g., "@repo/ui": "workspace:*") and letting pnpm link it in node_modules. Reserving paths for app-internal aliases simplifies debugging when something breaks.
My take and the concrete next step
tsconfig paths in Next.js 16 App Router are useful when used for what they were designed for: internal aliases within the app, with baseUrl declared explicitly and a cold build verification. When you stretch them to resolve workspace packages or inherit them without checking the context, they become a source of errors that the tooling doesn't always communicate well.
I don't buy the "always use @/" recommendation without more context. And I don't buy the opposite extreme of avoiding aliases entirely either. The honest trade-off is this: aliases improve code readability, but they add an indirection layer that can diverge between tools. In a monorepo with multiple tsconfig.json files, that divergence is more likely.
The concrete next step if you're working with this: open the tsconfig.json for every workspace package, verify that baseUrl is declared explicitly, and run next build cold once. If the build passes, your aliases are stable. If it doesn't, you have the three cases above as a diagnostic guide.
If you want to go deeper on TypeScript configuration for production, I have a more detailed breakdown of the tsconfig options that have the most impact in production. And if you work with architectures that cross multiple services — where paths between modules become a design decision, not just a config detail — the context from backend architecture with JWT and OAuth is worth a read.
Original sources:
- TypeScript Docs — Path Mapping: https://www.typescriptlang.org/tsconfig#paths
- Next.js Docs — Absolute Imports and Module Path Aliases: https://nextjs.org/docs/app/getting-started/installation#set-up-absolute-imports-and-module-aliases
Related Articles
A native discriminated union already does what Either promises
I wrote about fp-ts this week and was left with an uncomfortable doubt: did I really need all that machinery? A look at when a native discriminated union solves the same problem as Either/Option without the learning curve.
Aug 14 2026 · 7′ · Tutorials · TypeScript · arquitectura de software
Functional programming with TypeScript: what fp-ts teaches you even if you never ship it
fp-ts is a university, not a production framework for most teams. But ignoring it completely means leaving genuinely valuable concepts on the table. An honest walkthrough of Option, Either, and pipe from the perspective of strict TypeScript in the real world — plus the uncomfortable question of whether you actually need it.
Aug 07 2026 · 10′ · Tutorials · Next.js · TypeScript
Qwen3 locally with Ollama: what changed in the architecture and whether it's worth switching
Qwen3 landed with thinking mode and real improvements in code generation. But before you replace the model already running in your Ollama setup, there are technical questions you need to answer first. I answer them here without selling hype.
Aug 02 2026 · 9′ · Tutorials · TypeScript · Inferencia Local
Comments (0)
What do you think of this?
Drop your comment in 10 seconds.
We only use your login to show your name and avatar. No spam.
No comments yet. Be the first — your take matters most when we're few.