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.tsxwithout 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