Next.js has one of the most powerful file-based routing systems in the frontend world. You create a file, you get a route. But there's a lot more depth to it than that — dynamic segments, catch-all routes, parallel routes, intercepting routes, and more.
This post covers every routing pattern in the App Router (Next.js 13+) with real examples so you know exactly when and how to use each one.
1. Static routing
The simplest kind. Create a file inside app/, and the path maps directly to the URL.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── contact/
└── page.tsx → /contact// app/about/page.tsx
export default function AboutPage() {
return <h1>About us</h1>
}That's it. No configuration. The folder name becomes the URL segment.
2. Dynamic routing — [param]
When you don't know the path ahead of time — like a user profile or a product page — use a dynamic segment by wrapping the folder name in square brackets.
app/
└── users/
└── [id]/
└── page.tsx → /users/1, /users/42, /users/abc// app/users/[id]/page.tsx
type Props = {
params: { id: string }
}
export default function UserPage({ params }: Props) {
return <h1>User ID: {params.id}</h1>
}The value in the URL becomes available via params. So visiting /users/99 gives you params.id === "99".
Fetching data with dynamic params
// app/users/[id]/page.tsx
async function getUser(id: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
}
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getUser(params.id)
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}3. Slug routing — [slug]
A slug is just a dynamic segment with a semantic name. Typically used for blog posts, articles, or any human-readable URL.
app/
└── blog/
└── [slug]/
└── page.tsx → /blog/my-first-post, /blog/nextjs-routing-guide// app/blog/[slug]/page.tsx
type Props = {
params: { slug: string }
}
async function getPost(slug: string) {
// fetch from your CMS, MDX files, DB, etc.
const res = await fetch(`https://api.example.com/posts/${slug}`)
return res.json()
}
export default async function BlogPost({ params }: Props) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<p>{post.description}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}Generating static pages with generateStaticParams
If you want Next.js to pre-render all blog posts at build time:
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}Next.js will call generateStaticParams, get all slugs, and pre-render each one as a static page. Fast and SEO-friendly.
4. Nested dynamic routing
You can nest dynamic segments for more complex URL structures.
app/
└── blog/
└── [category]/
└── [slug]/
└── page.tsx → /blog/javascript/understanding-closures// app/blog/[category]/[slug]/page.tsx
type Props = {
params: {
category: string
slug: string
}
}
export default async function BlogPost({ params }: Props) {
const { category, slug } = params
return (
<div>
<span>Category: {category}</span>
<h1>{slug}</h1>
</div>
)
}Visiting /blog/javascript/understanding-closures gives:
params.category === "javascript"params.slug === "understanding-closures"
5. Catch-all routes — [...slug]
A catch-all segment matches any number of path segments from that point on.
app/
└── docs/
└── [...slug]/
└── page.tsxThis single file handles all of:
/docs/intro/docs/guides/installation/docs/api/reference/components
// app/docs/[...slug]/page.tsx
type Props = {
params: { slug: string[] }
}
export default function DocsPage({ params }: Props) {
// params.slug is an array of segments
// /docs/guides/installation → ['guides', 'installation']
return (
<div>
<p>Path: {params.slug.join(' / ')}</p>
</div>
)
}
params.slugis always an array with catch-all routes — not a string.
6. Optional catch-all routes — [[...slug]]
Same as catch-all, but also matches the root path with no segments.
app/
└── shop/
└── [[...slug]]/
└── page.tsxThis handles:
/shop→params.slug === undefined/shop/shoes→params.slug === ['shoes']/shop/shoes/nike→params.slug === ['shoes', 'nike']
// app/shop/[[...slug]]/page.tsx
type Props = {
params: { slug?: string[] }
}
export default function ShopPage({ params }: Props) {
if (!params.slug) {
return <h1>All Products</h1>
}
return <h1>Category: {params.slug.join(' > ')}</h1>
}The difference between [...slug] and [[...slug]]:
[...slug]—/shopreturns 404,/shop/anythingworks[[...slug]]— both/shopand/shop/anythingwork
7. Route groups — (group)
Route groups let you organize files without affecting the URL. Wrap a folder name in parentheses and it's invisible to the router.
app/
└── (marketing)/
├── about/
│ └── page.tsx → /about
└── pricing/
└── page.tsx → /pricing
└── (dashboard)/
├── settings/
│ └── page.tsx → /settings
└── analytics/
└── page.tsx → /analyticsThis is great for:
- Separate layouts — marketing pages get a different layout than dashboard pages
- Code organization — group related routes without polluting the URL
// app/(dashboard)/layout.tsx — only applies to dashboard routes
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
)
}8. Parallel routes — @slot
Parallel routes let you render multiple pages simultaneously in the same layout — like a dashboard with independent panels that each have their own loading/error states.
app/
└── dashboard/
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── layout.tsx// app/dashboard/layout.tsx
type Props = {
analytics: React.ReactNode
team: React.ReactNode
}
export default function DashboardLayout({ analytics, team }: Props) {
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<section>{analytics}</section>
<section>{team}</section>
</div>
)
}Each slot (@analytics, @team) renders its own page.tsx independently. If one is loading or errors, it doesn't affect the other.
9. Intercepting routes — (.), (..), (...)
Intercepting routes let you load a route inside the current layout without a full page navigation. The classic example: opening a photo in a modal when clicked in a feed, but navigating directly to the full photo page when you refresh.
app/
└── feed/
├── page.tsx
├── (.)photo/
│ └── [id]/
│ └── page.tsx ← intercepts /photo/[id] when navigating from /feed
└── photo/
└── [id]/
└── page.tsx ← shown on direct visit or refreshThe notation:
(.)— intercepts a segment at the same level(..)— intercepts a segment one level up(..)(..)— intercepts two levels up(...)— intercepts from the root
// app/feed/(.)photo/[id]/page.tsx — shown as a modal
import { Modal } from '@/components/modal'
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<Modal>
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</Modal>
)
}// app/photo/[id]/page.tsx — shown on direct visit
export default function PhotoPage({ params }: { params: { id: string } }) {
return (
<div>
<img src={`/photos/${params.id}.jpg`} alt="Photo" />
</div>
)
}10. Private folders — _folder
Prefix a folder with _ and Next.js completely ignores it for routing. Useful for colocating components, utils, or tests next to your routes.
app/
└── blog/
├── _components/
│ └── PostCard.tsx ← not a route, just a component
├── [slug]/
│ └── page.tsx
└── page.tsxQuick reference
| Pattern | Example | Matches |
|---|---|---|
| Static | app/about/page.tsx | /about |
| Dynamic | app/users/[id]/page.tsx | /users/1 |
| Slug | app/blog/[slug]/page.tsx | /blog/my-post |
| Nested dynamic | app/blog/[cat]/[slug]/page.tsx | /blog/js/closures |
| Catch-all | app/docs/[...slug]/page.tsx | /docs/a/b/c |
| Optional catch-all | app/shop/[[...slug]]/page.tsx | /shop, /shop/a/b |
| Route group | app/(marketing)/about/page.tsx | /about |
| Parallel | app/dash/@analytics/page.tsx | slot in layout |
| Intercepting | app/feed/(.)photo/[id]/page.tsx | modal intercept |
| Private folder | app/blog/_components/ | not a route |
Next.js routing looks simple on the surface but has a surprising amount of depth. Static routes get you 80% of the way. Dynamic segments and slugs cover most real-world apps. Catch-all routes handle documentation and nested taxonomies. And parallel + intercepting routes unlock UI patterns that used to require a lot of custom state management.
Start simple, reach for the advanced patterns only when you need them.