Skip to content

Next Routing Deep Dive

File-based routing where the folder IS the URL and your entire navigation structure lives in the directory tree

No routing config. No React Router. No react-router-dom dependency. The filesystem IS your route map and that means you can figure out every page in the application by running find src/app -name "page.tsx". Clean. Predictable. Easy to audit

file-based routing basics

Every folder in src/app/ adds a segment to the URL path. Every page.tsx file renders content for that path

src/app/
  page.tsx              -> /
  about/page.tsx        -> /about
  blog/page.tsx         -> /blog
  contact/page.tsx      -> /contact

The folder nesting maps directly to URL nesting:

// src/app/blog/page.tsx
export default function Blog() {
  return <h1>Blog Index</h1>
}

To change a URL structure , rename a folder or move it. No route file to edit. That's the whole point

dynamic routes - [param]

Square brackets in folder names create dynamic segments

src/app/
  blog/
    [slug]/
      page.tsx          -> /blog/hello-world , /blog/any-slug

The folder [slug] captures any value in that segment and passes it to the page component

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

  // fetch post by slug
  const post = await getPostBySlug(slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

Important: In Next.js 15+ , params is a Promise. You must await it. This is different from earlier versions where params was a synchronous object

Multiple dynamic params:

src/app/
  products/
    [category]/
      [productId]/
        page.tsx        -> /products/electronics/42
// src/app/products/[category]/[productId]/page.tsx
export default async function ProductPage({
  params
}: {
  params: Promise<{ category: string; productId: string }>
}) {
  const { category, productId } = await params
  // fetch product by category and ID
}

catch-all routes

Three dots in the brackets capture remaining segments

src/app/
  docs/
    [...slug]/
      page.tsx          -> /docs/a , /docs/a/b , /docs/a/b/c
// src/app/docs/[...slug]/page.tsx
export default async function DocsPage({
  params
}: {
  params: Promise<{ slug: string[] }>
}) {
  const { slug } = await params
  // slug is an array: ['a', 'b', 'c'] for /docs/a/b/c

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

Optional catch-all uses double brackets:

src/app/
  docs/
    [[...slug]]/
      page.tsx          -> /docs , /docs/a , /docs/a/b

The difference: [...slug] requires at least one segment. [[...slug]] matches the root path too

route groups

Parentheses in folder names group routes without affecting the URL

src/app/
  (marketing)/
    page.tsx             -> /
    about/page.tsx       -> /about
  (dashboard)/
    dashboard/
      page.tsx           -> /dashboard
    settings/
      page.tsx           -> /dashboard/settings
  (auth)/
    login/page.tsx       -> /login
    register/page.tsx    -> /register

Route groups let you:

  • Organize by feature - all marketing pages in one group , dashboard in another
  • Different layouts - each group can have its own layout.tsx without affecting other groups
  • Skip middleware - middleware can target specific route groups (more on this in the middleware section)

Security warning: Route groups can accidentally bypass middleware if your matcher config doesn't account for the actual URL paths. The group name exists only for organization - it doesn't appear in the URL. If your middleware checks for (dashboard) it won't match /dashboard

parallel routes

Multiple page components render simultaneously in the same layout using slots

src/app/
  @feed/
    page.tsx             -> renders in "feed" slot
    @notifications/
      page.tsx           -> renders in "notifications" slot
  layout.tsx             -> receives feed and notifications as props
// src/app/layout.tsx
export default function Layout({
  feed,
  notifications
}: {
  feed: React.ReactNode
  notifications: React.ReactNode
}) {
  return (
    <div className="flex">
      <aside>{feed}</aside>
      <aside>{notifications}</aside>
    </div>
  )
}

Parallel routes are useful for dashboards with independent sections that load separately. Each slot has its own loading and error states

intercepting routes

(.) prefix intercepts routes at the same level , (..) one level up , (..)(..) two levels up

src/app/
  feed/
    page.tsx
    photo/
      [id]/
        page.tsx         -> /feed/photo/123
  (.)feed/
    photo/
      [id]/
        page.tsx         -> intercepts /feed/photo/123 when navigated from within feed

This is how modals work. Clicking a photo in a feed shows a modal with the photo but the URL changes to /feed/photo/123. Refreshing the page renders the actual photo page instead

'use client'

import { useRouter } from 'next/navigation'

export default function PhotoModal({ params }: { params: { id: string } }) {
  const router = useRouter()

  return (
    <div className="modal" onClick={() => router.back()}>
      <img src={`/photos/${params.id}.jpg`} />
      <button onClick={() => router.back()}>Close</button>
    </div>
  )
}

Intercepting routes are wild. Use them for gallery views , social media feeds , or any place where you want a modal that also functions as a real page

prerequisites

next_02_get_started.md - you should understand layouts , pages , and basic navigation


next → next_04_navigation.md