A next.js 404 on a route that exists on disk and looks correct is one of the more confusing App Router problems to debug, because the file tree offers no obvious clue. This walks through the specific causes worth checking, in the order they are worth ruling out.
No page.tsx in the folder
The most common cause is also the simplest: the folder exists, but nothing inside it makes it public. A route only becomes reachable once it contains a page file or a route file, as covered in how file-based routing works in the Next.js App Router.
app/
dashboard/
settings/
layout.tsx -> exists, but adds no pageVisiting /dashboard/settings 404s here because a layout file alone never makes a segment public. Add a page file next to it to fix this.
dynamicParams set to false
If a dynamic route exports generateStaticParams, any value that function did not return still renders normally by default. Setting dynamicParams to false changes that, and any value outside the returned list becomes a real 404 instead.
// app/blog/[slug]/page.tsx
export const dynamicParams = false;A slug that exists in the underlying data source, but was left out of the returned array, 404s here even though the page's own logic never rejected it and the surrounding data fetch works exactly as expected. Removing this line, or adding the missing value to the returned list, fixes it. The full return shape for that function is covered in generateStaticParams explained with real examples.
A route group path collision
Two different route groups resolving to the same URL fail the entire build, not just the one route that looks wrong.
app/(marketing)/about/page.tsx
app/(shop)/about/page.tsxBoth resolve to /about, and Next.js rejects the build with a conflicting path error rather than picking one silently. Rename or move one of the two pages so only one folder produces that path. This one is easier to rule out than most causes on this list, since a genuine collision surfaces immediately in the build log rather than showing up later as a mysterious 404 in production.
A private folder hiding the segment
A folder prefixed with an underscore opts itself, and everything nested inside it, out of routing entirely. A page.tsx accidentally placed inside a private folder never becomes reachable, no matter how correctly it is written.
app/
_blog/
page.tsx -> never routable, the parent folder is privateRenaming the folder to drop the underscore prefix restores normal routing for everything inside it. This particular cause tends to surface after someone copies a folder structure from another project or an example repository without checking whether the underscore convention was intentional there, or already in active use for a different purpose.
Missing default.js in a parallel route slot
For the implicit children slot in a layout using parallel routes, a missing default.tsx produces a 404 for the whole route on a hard navigation, even though the page itself renders correctly on a normal client-side click. Adding one, even a minimal file that returns null, resolves it. Named slots behave differently and are covered in default.js explained.
Checklist
| Symptom | Likely cause |
|---|---|
| Folder exists, route still 404s | Missing page.tsx or route.ts |
| Only some dynamic values 404 | dynamicParams is false |
| Build fails instead of just one route | Route group path collision |
| Nothing under a folder ever routes | Private, underscore-prefixed folder |
Work through this list in order before assuming a data or code bug, since routing configuration causes far more unexpected 404s than actual application logic does. Checking the file tree first also tends to be faster than adding logging or stepping through a debugger, because most of these causes are visible the moment you look at the right folder with the right expectation in mind.
Rune AI
Key Insights
- A folder needs a page.tsx or route.ts file to be reachable at all.
- dynamicParams set to false 404s any value generateStaticParams did not return.
- Two route groups resolving to the same path fail the build, not just one route.
- A private, underscore-prefixed folder is invisible to routing entirely.
- A missing default.js 404s only the implicit children slot in a parallel route.
Frequently Asked Questions
Does a missing default.js in a parallel route cause a 404?
Can a typo in a dynamic folder name cause this?
Is dynamicParams the same as generateStaticParams?
Conclusion
An unexpected 404 in the App Router almost always traces back to one of a small number of causes: a missing page file, a dynamicParams override, a route group path collision, or a private folder hiding the segment entirely. Check the file tree before assuming the code is wrong.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.