Hello folks, as promised, back with the blog of Next.js file-based routing.
Next.js has one of the most elegant routing systems in the React ecosystem: file-system based routing. Instead of manually declaring routes in a configuration file, the structure of your folders and files is the routing structure. This blog focuses exclusively on the modern App Router (app/ directory) and covers every major routing convention in detail.
Introduction – File-Based Routing & App Router vs Pages Router
Traditionally, you often write something like:
router.get('/blog/:slug', handler)
Next.js flips this model. You create a folder named blog, put a file inside it, and the route appears automatically.
There are two routing systems in Next.js:
-
Pages Router (
pages/directory) – the original system (still fully supported). -
App Router (
app/directory) – introduced in Next.js 13, built on React Server Components, nested layouts, streaming, and more powerful conventions.
This guide uses the App Router, which is the recommended approach for all new projects.
Core Mental Model
Three rules govern almost everything:
Folders = Route Segments
Every folder insideapp/becomes a route segment in the URL.page.tsx(orpage.jsx) makes a route public
A folder without apage.tsxis not a public route. It can still contain nested layouts, components, or other files.-
Special files create UI and behavior
-
layout.tsx→ shared UI that wraps the segment and all children (can be global or nested) -
loading.tsx→ loading UI -
error.tsx→ error boundary -
not-found.tsx→ 404 UI (can be global or nested)
-
Folders + Files = URLs
With these rules in mind, let’s explore each routing pattern.
1. Static Routes + Nested Static Routes
Folder structure
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ └── page.tsx → /blog
└── dashboard/
├── page.tsx → /dashboard
├── analytics/
│ └── page.tsx → /dashboard/analytics
└── settings/
├── page.tsx → /dashboard/settings
└── profile/
└── page.tsx → /dashboard/settings/profile
Static routes are the simplest form: the folder name becomes the exact URL segment. Nesting folders creates nested URLs. Each level can optionally have its own layout.tsx that only wraps that segment and its children.
Key technical notes
- The root
app/page.tsxis required for the homepage (/). - A folder without
page.tsxis invisible to the router (great for colocation). - Layouts nest automatically: root layout → dashboard layout → settings layout.
2. Dynamic Routes (Single Segment)
Folder structure
app/
└── blog/
├── page.tsx → /blog
└── [slug]/
└── page.tsx → /blog/:slug
A dynamic segment is created by wrapping the folder name in square brackets: [slug]. The value of slug is extracted from the URL and passed to your page.
Accessing the parameter (server component)
// app/blog/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Blog Post: {slug}</h1>;
}
Key technical notes
- In recent Next.js versions,
paramsis a Promise — you mustawaitit. - Client Components can use the
useParams()hook fromnext/navigation. - Use
generateStaticParams()to pre-render known dynamic paths at build time (covered later).
3. Multiple Dynamic Route Segments
Folder structure
app/
└── shop/
└── [category]/
└── [productId]/
└── page.tsx → /shop/:category/:productId
You can stack as many dynamic segments as you need. Each [param] is independent and appears in the params object.
Example
| URL | params |
|---|---|
/shop/electronics/iphone-15 |
{ category: "electronics", productId: "iphone-15" } |
/shop/clothing/nike-air |
{ category: "clothing", productId: "nike-air" } |
export default async function Page({
params,
}: {
params: Promise<{ category: string; productId: string }>;
}) {
const { category, productId } = await params;
// fetch product data using both values
}
You can freely mix static and dynamic segments (e.g. /shop/[category]/reviews/[reviewId]).
4. Catch-All Routes
Folder structure
app/
└── docs/
└── [...slug]/
└── page.tsx → /docs/* (one or more segments)
The [...slug] syntax (ellipsis inside brackets) captures one or more segments as an array. This is introduced because when we don't know about the depth of level in multiple dynamic routes then this is the best option to choose.
| URL | params.slug |
|---|---|
/docs/getting-started |
['getting-started'] |
/docs/api/auth |
['api', 'auth'] |
/docs/a/b/c/d |
['a', 'b', 'c', 'd'] |
Important: A regular catch-all does not match the parent path /docs. That would 404 unless you also have a docs/page.tsx.
export default async function Page({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return <div>Path: {slug.join(' / ')}</div>;
}
5. Optional Catch-All Routes
Folder structure
app/
└── docs/
└── [[...slug]]/
└── page.tsx → /docs AND /docs/*
Double square brackets [[...slug]] make the catch-all optional. It now also matches the route with zero additional segments means it solves the problem of catch-all-routes also.
| URL | params.slug |
|---|---|
/docs |
undefined |
/docs/getting-started |
['getting-started'] |
/docs/api/auth |
['api', 'auth'] |
This pattern is extremely useful for documentation sites, knowledge bases, or any UI that needs a root view plus arbitrary depth.
Always handle the undefined case in your component.
Important: Don't ever use both the catch all routes and optional catch all routes together not under the same folder. Either use first one or the second one; but not together under same folder.
export default async function Page({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
return <div>Path: {slug ? slug.join('/') : '(no slug — matched /page itself)'}</div>;
}
6. Route Groups (+ Private Folders)
Folder structure
app/
├── (marketing)/
│ ├── page.tsx → /
│ ├── about/
│ │ └── page.tsx → /about
│ ├── pricing/
│ │ └── page.tsx → /pricing
│ └── layout.tsx // marketing layout only
├── (shop)/
│ ├── products/
│ │ └── page.tsx → /products
│ ├── cart/
│ │ └── page.tsx → /cart
│ └── layout.tsx // shop layout only
├── (dashboard)/
│ ├── layout.tsx // dashboard layout
│ └── settings/
│ └── page.tsx → /settings
└── layout.tsx // root layout
Route groups are created by wrapping a folder name in parentheses: (marketing). The parentheses and the name inside them never appear in the URL. They exist purely for organization and for applying different layouts to different sets of routes. If we don't want to show the folder structure in the route/URL itself, then we will use this.
Common use cases
- Separate marketing pages from authenticated dashboard pages
- Apply different root layouts
- Organize by team or feature without polluting the URL
Private folders (related convention)
Prefix any folder with an underscore (_components, _lib, _utils). These folders and everything inside them are completely ignored by the router — perfect for colocation.
Important rules
- Two different route groups cannot resolve to the same URL path (build-time error).
- Navigating between routes that use different root layouts triggers a full page load.
Practical Tips
Mixing conventions
You can combine everything freely:
app/
└── (shop)/
└── [category]/
└── [...slug]/
└── page.tsx
generateStaticParams
For dynamic and catch-all routes you often want static generation:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
Layouts interaction
- Layouts wrap their children and persist across navigations within the same segment.
- A layout inside a route group only applies to routes inside that group.
- Root layout is required (unless you intentionally create multiple root layouts via route groups).
Common pitfalls
- Forgetting that a folder needs a
page.tsxto be public. - Conflicting paths between two route groups.
- Not awaiting
paramsin newer Next.js versions. - Expecting a regular catch-all
[...slug]to match the parent path. - Putting business logic or shared components directly in route folders instead of using private folders or a top-level
components/directory.
Conclusion & Further Reading
File-based routing in the App Router is both powerful and surprisingly simple once the core mental model clicks: folders define segments, page.tsx makes them public, and special naming conventions ([], [...], [[...]], ()) give you dynamic behavior and organization.
Master these patterns and you can model almost any URL structure cleanly while keeping your project organized and performant.
Official documentation
Happy routing!






Top comments (0)