Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 6 min read

Mastering Next.js File-Based Routing (App Router)

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

Mastering Next.js File-Based Routing (App Router)

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:

  1. Folders = Route Segments

    Every folder inside app/ becomes a route segment in the URL.

  2. page.tsx (or page.jsx) makes a route public

    A folder without a page.tsx is not a public route. It can still contain nested layouts, components, or other files.

  3. 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.tsx is required for the homepage (/).
  • A folder without page.tsx is invisible to the router (great for colocation).
  • Layouts nest automatically: root layout β†’ dashboard layout β†’ settings layout.

Static & Nested Static Routes

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, params is a Promise β€” you must await it.
  • Client Components can use the useParams() hook from next/navigation.
  • Use generateStaticParams() to pre-render known dynamic paths at build time (covered later).

Dynamic Routes

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]).

Multiple Dynamic Routes

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>;
}

catch-all-routes

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>;
}

optional-catch-all-routes

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.

route-groups

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.tsx to be public.
  • Conflicting paths between two route groups.
  • Not awaiting params in 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!

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.