ProgrUmar Logo
Module 1: Foundations & Routing

File-based Routing Deep Dive

Duration: 18 mins

File-based Routing

Next.js derives your URL structure directly from your folder tree inside app/. This lesson covers every routing primitive you'll use in a real project.

Topics Covered

  • Static segments: app/about/page.tsx
  • Dynamic segments: [slug], [...slug], [[...slug]]
  • Route Groups with (groupName)
  • Parallel Routes using @slot folders
  • Intercepting Routes with (..) convention

File-based Routing Deep Dive

In the last lesson you saw that creating app/about/page.tsx gives you a /about route automatically. That's the simplest case. Real apps need more — dynamic URLs like /blog/my-post, search-style URLs like /shop/electronics/laptops, and ways to organize files without affecting the URL at all.

This lesson covers every routing primitive in the App Router. By the end, you'll be able to build any URL structure you can imagine.


1. Static Routes (Recap)

A folder with a page.tsx inside it becomes a route matching that folder's path:

app/
├── page.tsx              → /
├── about/
│   └── page.tsx           → /about
└── contact/
    └── page.tsx           → /contact

Nothing new here — but it's the foundation everything else builds on.


2. Dynamic Segments: [slug]

Most real apps need URLs that depend on data — a blog post ID, a username, a product SKU. Wrap a folder name in square brackets to create a dynamic segment:

app/
└── blog/
    └── [slug]/
        └── page.tsx       → /blog/anything-here

Whatever the user types after /blog/ gets captured as a parameter called slug. You access it through the params prop:

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  return (
    <article>
      <h1>Post: {slug}</h1>
    </article>
  );
}

Important: as of Next.js 15, params is a Promise and must be awaited. This changed from earlier versions where it was a plain object — a common source of confusion if you're following older tutorials.

Visiting /blog/hello-world renders "Post: hello-world". Try it yourself:

  • /blog/my-first-post → slug = "my-first-post"
  • /blog/nextjs-tips → slug = "nextjs-tips"

Fetching Real Data with the Slug

Combine what you learned about Server Components with the dynamic segment:

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`);

  if (!res.ok) {
    // We'll cover notFound() properly in a later lesson
    return <p>Post not found.</p>;
  }

  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

3. Catch-all Segments: [...slug]

Sometimes one parameter isn't enough. Imagine a documentation site with nested paths like /docs/getting-started/installation or /docs/api/routing/dynamic-segments. Use three dots inside the brackets to catch any number of segments:

app/
└── docs/
    └── [...slug]/
        └── page.tsx
// app/docs/[...slug]/page.tsx
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params;
  // slug is now an array, e.g. ["getting-started", "installation"]

  return (
    <div>
      <h1>Docs Path</h1>
      <p>{slug.join(" / ")}</p>
    </div>
  );
}
URLslug value
/docs/intro["intro"]
/docs/api/routing["api", "routing"]
/docs/a/b/c/d["a", "b", "c", "d"]

Note: /docs itself (with nothing after it) will not match [...slug] — you need at least one segment. We'll fix that next.


4. Optional Catch-all Segments: [[...slug]]

Add a second pair of square brackets to make the catch-all optional. This means the route also matches the base path with zero segments:

app/
└── docs/
    └── [[...slug]]/
        └── page.tsx
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug } = await params;

  if (!slug) {
    return <h1>Docs Home</h1>; // matches /docs exactly
  }

  return <h1>Docs: {slug.join(" / ")}</h1>;
}

Now /docs, /docs/intro, and /docs/api/routing are all handled by this single file.


5. Route Groups: (groupName)

Sometimes you want to organize files into folders without that folder name appearing in the URL. Wrap the folder name in parentheses to create a route group:

app/
├── (marketing)/
│   ├── layout.tsx        ← Layout just for marketing pages
│   ├── page.tsx           → /
│   ├── about/
│   │   └── page.tsx       → /about
│   └── pricing/
│       └── page.tsx       → /pricing
└── (shop)/
    ├── layout.tsx        ← Different layout for shop pages
    └── products/
        └── page.tsx       → /products

Notice (marketing) and (shop) don't appear in any URL — they're purely organizational. This is incredibly useful for two things:

  • Different layouts for different sections of your site without changing the URL structure.
  • Organizing a large codebase into logical folders that mirror your team structure or feature areas.

A very common pattern is separating authenticated and public layouts:

app/
├── (public)/
│   ├── layout.tsx        ← Public nav (Login / Sign Up buttons)
│   └── page.tsx
└── (authenticated)/
    ├── layout.tsx        ← Authenticated nav (Profile / Logout)
    └── dashboard/
        └── page.tsx

6. Private Folders: _folderName

Prefix a folder with an underscore to exclude it from routing entirely — useful for colocating helper files, components, or utilities next to the routes that use them without Next.js trying to treat them as a route:

app/
└── dashboard/
    ├── _components/        ← NOT a route, just colocated files
    │   ├── Sidebar.tsx
    │   └── Header.tsx
    └── page.tsx             → /dashboard

7. Parallel Routes: @slot

Parallel routes let you render multiple independent pages in the same layout simultaneously — think of a dashboard with a main feed and a separate analytics panel, each with its own loading and error states.

app/
└── dashboard/
    ├── layout.tsx
    ├── page.tsx
    ├── @analytics/
    │   └── page.tsx
    └── @team/
        └── page.tsx

The @slotName folders are passed into the layout as named props:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      {children}
      <div style={{ display: "flex" }}>
        <div>{analytics}</div>
        <div>{team}</div>
      </div>
    </div>
  );
}

Each slot loads, errors, and streams independently. If @analytics is slow, it won't block @team from rendering. This is an advanced pattern — you likely won't need it often, but it's powerful for complex dashboards.


8. Intercepting Routes: (..)

Intercepting routes let you show a route in a different context — the classic example is Instagram-style photo modals. Click a photo from a feed and it opens in a modal over the feed, but if you refresh that same URL, you get the full standalone page.

app/
├── feed/
│   └── page.tsx
└── photo/
    └── [id]/
        └── page.tsx        ← Full page version: /photo/123
└── @modal/
    └── (..)photo/
        └── [id]/
            └── page.tsx    ← Intercepted modal version

The (..) convention means "match a segment one level up in the URL, but render it here instead." The dot-count works like relative file paths:

ConventionMatches
(.)Same level
(..)One level up
(..)(..)Two levels up
(...)Root app/ directory

This is an advanced pattern usually combined with parallel routes. We'll build a real modal example together later in the course once you're comfortable with the basics.


9. Putting It Together: A Realistic Example

Here's a structure combining several of these techniques for an e-commerce site:

app/
├── layout.tsx
├── page.tsx                         → /
├── (shop)/
│   ├── products/
│   │   └── page.tsx                  → /products
│   └── product/
│       └── [id]/
│           └── page.tsx              → /product/42
├── (account)/
│   ├── layout.tsx                   ← Account-specific layout
│   ├── orders/
│   │   └── page.tsx                  → /orders
│   └── settings/
│       └── page.tsx                  → /settings
└── docs/
    └── [[...slug]]/
        └── page.tsx                  → /docs, /docs/faq, /docs/shipping/returns

Notice how route groups (shop) and (account) organize the code without affecting the URLs, while [id] and [[...slug]] handle dynamic content.


10. Common Gotchas

  • Don't mix a static and dynamic segment at the same level. You can't have both app/blog/featured/page.tsx and app/blog/[slug]/page.tsx if a visit to /blog/featured would be ambiguous — Next.js will throw a build error if segment names conflict in confusing ways. Generally, static segments take priority.
  • Forgetting to await params is the #1 mistake when upgrading to Next.js 15 — older code and tutorials treat params as a plain object.
  • Route group names must be unique within the same level if they'd otherwise produce the same URL — Next.js will error if two different route groups try to define the same route.
  • Parallel and intercepting routes are advanced. Don't reach for them until you have a concrete need — most apps never use them.

Key Takeaways

  • [slug] captures a single dynamic URL segment.
  • [...slug] catches multiple segments as an array; [[...slug]] makes that optional, including the base path.
  • (groupName) organizes files into folders without affecting the URL — great for layouts and code organization.
  • _folderName excludes a folder from routing entirely, for colocated helpers.
  • @slot folders enable parallel routes — multiple independent UI sections in one layout.
  • (..) conventions enable intercepting routes — useful for modal patterns.
  • In Next.js 15, params is always a Promise and must be awaited.

In the next lesson, we'll cover layouts and templates in more depth — including how to share UI across nested routes and when a template (which re-mounts) is the better choice over a layout (which persists).

Chat with us