Dynamic routes are one of those Next.js things that feel obvious right up until we’re staring at an app/ folder at 1:17 AM thinking, “Why is [slug] not slugging?” (Yes, we’ve been there. No, we didn’t handle it with grace.
If you’re using the Next.js App Router, dynamic routing is clean, powerful, and surprisingly flexible—once we learn the rules of the filesystem game. In this guide, we’ll walk through:
- How dynamic segments work in the App Router
- Single dynamic routes (
[slug]) - Catch-all routes (
[...slug]) and optional catch-all ([[...slug]]) - Reading
paramsin Server Components anduseParams()in Client Components - Pre-rendering with
generateStaticParams() - Dynamic SEO with
generateMetadata() - Bonus: dynamic routes in Route Handlers (
route.ts) for APIs
We’ll keep it practical, slightly whimsical, and very “we’ve shipped this in production” (because we have)—especially for teams building SaaS apps and dashboards across the USA, UK, Israel, Switzerland, and the UAE.
What Is a Dynamic Route in the Next.js App Router?
In the App Router, routes are created by folders, not by configuration. To make a route dynamic, we wrap a folder name in square brackets:
-
app/blog/[slug]/page.tsx→ matches/blog/hello-world,/blog/nextjs-routing, etc.
This is the official convention: wrap the segment name in [] to create a dynamic segment.
1) Basic Dynamic Route: app/blog/[slug]/page.tsx
Let’s build a classic blog post route:
Folder structure
app/
blog/
[slug]/
page.tsx
page.tsx (Server Component by default)
// app/blog/[slug]/page.tsx
type PageProps = {
params: { slug: string }
}
export default async function BlogPostPage({ params }: PageProps) {
// In real life: fetch from DB/CMS using params.slug
return (
<main>
<h1>Blog Post: {params.slug}</h1>
<p>(Yes, that slug is coming from the URL. The internet is magic.)</p>
</main>
)
}
Where does params come from?
Next.js injects params into your page based on the dynamic segment name. This is part of the App Router’s “Dynamic APIs” concept—params and searchParams are first-class route inputs.
Small but important 2026-ish note: Next.js has been moving some “dynamic APIs” toward async behavior (depending on version and usage). If you see warnings around sync usage, check the Next.js message docs and upgrade notes.
2) Reading Dynamic Params in Client Components with useParams()
Sometimes we need the slug on the client (e.g., for UI state, analytics, client-side interactions). In that case, use useParams() from next/navigation.
'use client'
import { useParams } from 'next/navigation'
export default function ClientWidget() {
const params = useParams<{ slug: string }>()
return <div>Client saw slug: {params.slug}</div>
}
useParams() is specifically a Client Component hook for reading dynamic params.
3) Catch-All Dynamic Routes: [...slug]
Catch-all routes match multiple path segments.
Example
app/docs/[...slug]/page.tsx
This matches:
/docs/getting-started/docs/app-router/dynamic-routes/docs/a/b/c
In App Router terminology, these are Catch-all Segments.
Implementation
// app/docs/[...slug]/page.tsx
type Props = {
params: { slug: string[] }
}
export default function DocsPage({ params }: Props) {
const path = params.slug.join('/')
return (
<main>
<h1>Docs Path</h1>
<p>Requested: /docs/{path}</p>
</main>
)
}
4) Optional Catch-All Routes: [[...slug]]
Optional catch-all routes are like catch-all routes, except they also match the base route.
Example
app/shop/[[...slug]]/page.tsx
Matches:
/shop/shop/clothes/shop/clothes/tops
This is exactly what Next.js describes for optional catch-all segments.
Implementation
// app/shop/[[...slug]]/page.tsx
type Props = {
params: { slug?: string[] }
}
export default function ShopPage({ params }: Props) {
const slug = params.slug ?? []
return (
<main>
<h1>Shop</h1>
<p>Category path: {slug.length ? slug.join('/') : '(root)'}</p>
</main>
)
}
5) Pre-render Dynamic Routes with generateStaticParams()
If your dynamic routes are known ahead of time (blog slugs, product IDs, docs pages), you can statically generate them at build time using generateStaticParams().
Next.js explicitly supports generateStaticParams for pages, layouts, and even route handlers.
Example
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
// Pretend these came from a CMS
return [
{ slug: 'hello-world' },
{ slug: 'nextjs-app-router' },
{ slug: 'dynamic-routes-that-finally-make-sense' },
]
}
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>
}
When should we use it?
Use generateStaticParams() when:
- you have a finite set of known pages
- you want fast load times and caching benefits
- content doesn’t change every minute
If data changes frequently, you may prefer on-demand rendering or caching strategies (that’s a whole other rabbit hole—with snacks).
6) SEO for Dynamic Routes with generateMetadata()
Dynamic routes usually need dynamic titles, descriptions, Open Graph tags, etc. That’s where generateMetadata() shines.
Next.js recommends generateMetadata() when metadata depends on route params or external data.
Example
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const title = params.slug.replace(/-/g, ' ')
return {
title: `${title} | Kanhasoft`,
description: `Read about ${title} and how we build scalable apps for teams in the USA, UK, Israel, Switzerland, and the UAE.`,
}
}
7) Dynamic Routes in Route Handlers (API): route.ts
If you’re using the App Router, Route Handlers are the “API routes, but App Router style.” They’re only available inside app/.
Example structure
app/api/posts/[id]/route.ts
Implementation
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
return Response.json({ postId: params.id })
}
Route handlers support standard HTTP methods like GET/POST/etc.
Also worth knowing: caching behavior for GET Route Handlers has changed across versions (especially around Next 15 defaults), so treat caching as a deliberate choice, not an assumption.
Common “Wait, Why Isn’t This Working?” Mistakes (We’ve Made Them Too)
1) Putting page.tsx in the wrong folder
app/blog/[slug].tsx ❌ (wrong)app/blog/[slug]/page.tsx ✅ (right)
2) Mixing Pages Router habits with App Router patterns
App Router = folder-based routing + Server Components by default.
3) Expecting useRouter() for everything
In App Router, prefer next/navigation hooks like useParams() for client params.
4) Forgetting catch-all params are arrays
[...slug] → slug: string[] (not string)
5) Optional catch-all can be undefined
[[...slug]] → slug?: string[]
A Simple Pattern We Use on Real Projects
For production-grade dynamic routes (SaaS dashboards, CRMs, portals), we typically do this:
- Route param drives data fetch (server-side)
generateMetadata()uses the same fetch (or minimal fetch)generateStaticParams()only if content is stable- Client components only for interactivity (filters, modals, charts)
It keeps things fast, consistent, and less likely to implode during a launch week (we like calm launches—rare, but we try).
Conclusion: Dynamic Routes Are Simple… After We Respect the Folder Rules
Once we embrace that the App Router is basically a filesystem-powered routing machine (with opinions), dynamic routes become one of the nicest parts of Next.js.
Use [slug] for single segments, [...slug] when you need multiple segments, and [[...slug]] when you want to catch “root + nested paths.” Add generateStaticParams() when you can prebuild, and generateMetadata() when you care about SEO (which we do—especially for audiences across the USA, UK, Israel, Switzerland, and the UAE).
And if it still breaks? It’s probably the folder name. (We say this lovingly, from experience.)
FAQs: Dynamic Routes in Next.js App Router
Q. How do we create a dynamic route in the App Router?
A. Create a folder with brackets, like app/blog/[slug]/page.tsx. Next.js uses the folder name as the param key.
Q. How do we read route params in an App Router page?
A. Use the params prop in your Server Component page:export default function Page({ params }) { ... }
Dynamic APIs include params and searchParams.
Q. How do we read params in a Client Component?
A. Use useParams() from next/navigation.
Q. What’s the difference between [...slug] and [[...slug]]?
A.
-
[...slug]is catch-all and requires at least one segment. -
[[...slug]]is optional catch-all and also matches the base route.
Q. How do we statically pre-render dynamic routes?
A. Export generateStaticParams() from the route segment to generate paths at build time.
Q. How do we set dynamic SEO metadata per slug?
A. Use generateMetadata() and access params.slug.
Q. Can Route Handlers be dynamic too?
A. Yes. You can use dynamic segments in app/api/.../[id]/route.ts and access params. Route handlers are App Router’s API mechanism.


