Next.js Dynamic Routes: Complete [slug] Guide with Examples
1. What Are Dynamic Routes?
Dynamic routes let you create a single page component that renders multiple URLs based on variable path segments. Instead of creating a separate file for every course, blog post, or product, you create one template file that adapts to any value.
Static Route: /about → app/about/page.tsx
Dynamic Route: /courses/python → app/courses/[slug]/page.tsx
/courses/react → Same file, different data
/courses/nextjs → Same file, different data
The [slug] folder acts as a placeholder. Next.js matches any value in that position and passes it to your component as a parameter.
2. Why Are Dynamic Routes Needed?
Imagine building a course platform with 100 courses. Without dynamic routes:
app/courses/
├── python/
│ └── page.tsx ← Duplicate code
├── javascript/
│ └── page.tsx ← Duplicate code
├── nextjs/
│ └── page.tsx ← Duplicate code
└── react/
└── page.tsx ← Duplicate code
Problems:
- ❌ 100 nearly identical files
- ❌ Updating the layout means editing 100 files
- ❌ Adding a new course requires creating a new file
- ❌ Impossible to scale
With dynamic routes:
app/courses/
└── [slug]/
└── page.tsx ← One file handles all 100+ courses
Benefits:
- ✅ One template, infinite URLs
- ✅ Data-driven from a database or API
- ✅ New courses work instantly — no code changes
- ✅ SEO-friendly with unique metadata per page
3. Basic Syntax
Create a folder with square brackets [paramName] inside your route structure:
app/
└── courses/
└── [slug]/ ← Dynamic segment
└── page.tsx
The page.tsx receives params as a prop:
1interface Props { 2 params: Promise<{ slug: string }>; 3} 4 5export default async function Page({ params }: Props) { 6 const { slug } = await params; 7 // slug = "python", "javascript", "nextjs", etc. 8}
URL → Params Mapping:
| URL | params.slug |
|---|---|
/courses/python | "python" |
/courses/javascript | "javascript" |
/courses/nextjs | "nextjs" |
/courses/system-design | "system-design" |
4. Simple Example
Let's start with a minimal dynamic route:
1// app/courses/[slug]/page.tsx 2interface Props { 3 params: Promise<{ slug: string }>; 4} 5 6export default async function CoursePage({ params }: Props) { 7 const { slug } = await params; 8 9 return ( 10 <main className="max-w-4xl mx-auto px-4 py-12"> 11 <h1 className="text-4xl font-bold mb-4"> 12 Course: {slug.replace(/-/g, " ")} 13 </h1> 14 <p className="text-gray-600"> 15 You are viewing the course with slug: <code>{slug}</code> 16 </p> 17 </main> 18 ); 19}
Visit /courses/python → Shows "Course: python"
Visit /courses/nextjs → Shows "Course: nextjs"
5. Next.js Example: Database-Driven Course Page
In production, the slug maps to real data from a database or API.
1// app/courses/[slug]/page.tsx 2import { notFound } from "next/navigation"; 3import Image from "next/image"; 4import Link from "next/link"; 5 6interface Props { 7 params: Promise<{ slug: string }>; 8} 9 10// Mock database function (replace with real API/DB call) 11async function getCourseBySlug(slug: string) { 12 const courses = [ 13 { 14 slug: "python", 15 title: "Python Fundamentals", 16 description: "Master Python from zero to hero.", 17 duration: "12 hours", 18 level: "Beginner", 19 thumbnail: "/images/python.jpg", 20 topics: ["Variables", "Functions", "OOP", "File Handling"], 21 }, 22 { 23 slug: "nextjs", 24 title: "Next.js Mastery", 25 description: "Build production apps with Next.js App Router.", 26 duration: "18 hours", 27 level: "Intermediate", 28 thumbnail: "/images/nextjs.jpg", 29 topics: ["Routing", "Server Components", "API Routes", "Deployment"], 30 }, 31 { 32 slug: "react", 33 title: "React Deep Dive", 34 description: "Advanced React patterns and performance.", 35 duration: "15 hours", 36 level: "Intermediate", 37 thumbnail: "/images/react.jpg", 38 topics: ["Hooks", "Context", "Performance", "Testing"], 39 }, 40 ]; 41 42 return courses.find((c) => c.slug === slug) ?? null; 43} 44 45export default async function CoursePage({ params }: Props) { 46 const { slug } = await params; 47 const course = await getCourseBySlug(slug); 48 49 if (!course) { 50 notFound(); // Shows the nearest not-found.tsx 51 } 52 53 return ( 54 <main className="max-w-5xl mx-auto px-4 py-12"> 55 <div className="flex flex-col md:flex-row gap-8"> 56 <div className="md:w-1/3"> 57 <Image 58 src={course.thumbnail} 59 alt={course.title} 60 width={400} 61 height={300} 62 className="rounded-xl shadow-lg" 63 priority 64 /> 65 </div> 66 <div className="md:w-2/3"> 67 <div className="flex items-center gap-3 mb-4"> 68 <span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium"> 69 {course.level} 70 </span> 71 <span className="text-gray-500 text-sm">{course.duration}</span> 72 </div> 73 <h1 className="text-4xl font-bold mb-4">{course.title}</h1> 74 <p className="text-lg text-gray-600 mb-8">{course.description}</p> 75 76 <h2 className="text-xl font-semibold mb-4">What You'll Learn</h2> 77 <ul className="grid grid-cols-2 gap-3"> 78 {course.topics.map((topic) => ( 79 <li key={topic} className="flex items-center gap-2"> 80 <span className="text-green-500">✓</span> 81 {topic} 82 </li> 83 ))} 84 </ul> 85 86 <Link 87 href={`/courses/${slug}/introduction`} 88 className="inline-block mt-8 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" 89 > 90 Start Learning → 91 </Link> 92 </div> 93 </div> 94 </main> 95 ); 96}
6. Real-World Example: Tech3Space Course Platform
A real learning platform needs dynamic metadata, lesson navigation, and topic breakdowns.
Dynamic SEO Metadata
1// app/courses/[slug]/page.tsx 2import { Metadata } from "next"; 3 4interface Props { 5 params: Promise<{ slug: string }>; 6} 7 8export async function generateMetadata({ params }: Props): Promise<Metadata> { 9 const { slug } = await params; 10 const course = await getCourseBySlug(slug); 11 12 if (!course) { 13 return { 14 title: "Course Not Found | Tech3Space", 15 }; 16 } 17 18 return { 19 title: `${course.title} | Tech3Space`, 20 description: course.description, 21 openGraph: { 22 title: course.title, 23 description: course.description, 24 images: [course.thumbnail], 25 }, 26 }; 27}
Course Listing Page Linking to Dynamic Routes
1// app/courses/page.tsx 2import Link from "next/link"; 3import Image from "next/image"; 4 5const courses = [ 6 { slug: "python", title: "Python Fundamentals", students: 12400 }, 7 { slug: "nextjs", title: "Next.js Mastery", students: 8900 }, 8 { slug: "react", title: "React Deep Dive", students: 15200 }, 9]; 10 11export default function CoursesPage() { 12 return ( 13 <main className="max-w-6xl mx-auto px-4 py-12"> 14 <h1 className="text-3xl font-bold mb-8">Explore Courses</h1> 15 <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> 16 {courses.map((course) => ( 17 <Link 18 key={course.slug} 19 href={`/courses/${course.slug}`} 20 className="group border rounded-xl overflow-hidden hover:shadow-xl transition" 21 > 22 <div className="h-48 bg-gray-100 relative"> 23 <Image 24 src={`/images/${course.slug}.jpg`} 25 alt={course.title} 26 fill 27 className="object-cover group-hover:scale-105 transition" 28 /> 29 </div> 30 <div className="p-5"> 31 <h2 className="text-xl font-semibold">{course.title}</h2> 32 <p className="text-gray-500 text-sm mt-1"> 33 {course.students.toLocaleString()} students enrolled 34 </p> 35 </div> 36 </Link> 37 ))} 38 </div> 39 </main> 40 ); 41}
7. Common Mistakes
❌ Mistake 1: Forgetting await on params
1// WRONG — params is a Promise in Next.js 15+ 2export default function Page({ params }: Props) { 3 const { slug } = params; // ❌ Missing await 4}
1// CORRECT 2export default async function Page({ params }: Props) { 3 const { slug } = await params; // ✅ Await the Promise 4}
❌ Mistake 2: No 404 Handling
1// WRONG — crashes if course doesn't exist 2export default async function Page({ params }: Props) { 3 const { slug } = await params; 4 const course = await getCourse(slug); 5 return <h1>{course.title}</h1>; // 💥 TypeError if course is null 6}
1// CORRECT — graceful 404 2export default async function Page({ params }: Props) { 3 const { slug } = await params; 4 const course = await getCourse(slug); 5 6 if (!course) { 7 notFound(); // ✅ Clean 404 page 8 } 9 10 return <h1>{course.title}</h1>; 11}
❌ Mistake 3: Using Client-Side Data Fetching for SEO Content
1// WRONG — bad for SEO, slower initial load 2"use client"; 3import { useEffect, useState } from "react"; 4 5export default function Page({ params }: Props) { 6 const [course, setCourse] = useState(null); 7 8 useEffect(() => { 9 fetch(`/api/courses/${params.slug}`).then((r) => r.json()).then(setCourse); 10 }, [params.slug]); 11 12 if (!course) return <div>Loading...</div>; 13 return <h1>{course.title}</h1>; 14}
1// CORRECT — server-rendered, SEO-friendly, instant HTML 2export default async function Page({ params }: Props) { 3 const { slug } = await params; 4 const course = await getCourse(slug); 5 if (!course) notFound(); 6 return <h1>{course.title}</h1>; 7}
❌ Mistake 4: Hardcoding Slugs in Navigation
1// WRONG — not scalable 2<Link href="/courses/python">Python</Link> 3<Link href="/courses/nextjs">Next.js</Link>
1// CORRECT — data-driven 2{courses.map((c) => ( 3 <Link key={c.slug} href={`/courses/${c.slug}`}> 4 {c.title} 5 </Link> 6))}
8. Best Practices
| Practice | Why It Matters |
|---|---|
| Always validate params | Prevents crashes from malformed URLs |
Use notFound() for missing data | Clean 404s instead of blank pages |
| Generate dynamic metadata | Unique SEO for every dynamic page |
| Fetch data in Server Components | Better SEO, faster initial paint |
Use generateStaticParams | Pre-render popular routes at build time |
| Type your params | Catches bugs at compile time |
| Handle loading states | Use loading.tsx for better UX |
Pre-Rendering with generateStaticParams
If you know all possible slugs at build time, tell Next.js to pre-render them:
1// app/courses/[slug]/page.tsx 2export async function generateStaticParams() { 3 const courses = await getAllCourses(); // Fetch from API/DB 4 5 return courses.map((course) => ({ 6 slug: course.slug, 7 })); 8} 9 10// Next.js will pre-render: 11// /courses/python 12// /courses/nextjs 13// /courses/react 14// at BUILD time for instant loading
9. Architecture Explanation
How a Dynamic Route Request Flows
User visits: /courses/nextjs
↓
Next.js Router matches: app/courses/[slug]/page.tsx
slug = "nextjs"
↓
Server Component executes (on the server)
↓
getCourseBySlug("nextjs") → API / Database
↓
Validate response (notFound() if missing)
↓
Render HTML with course data
↓
Send fully rendered HTML to browser
↓
Hydrate interactive Client Components
Dynamic Route Architecture for Tech3Space
src/
├── app/
│ └── courses/
│ ├── page.tsx ← Course listing (/courses)
│ └── [slug]/
│ ├── page.tsx ← Course detail (/courses/nextjs)
│ ├── loading.tsx ← Skeleton while loading
│ ├── error.tsx ← Error boundary
│ ├── not-found.tsx ← Invalid course slug
│ └── [topicSlug]/ ← Nested dynamic route
│ └── page.tsx ← Topic detail (/courses/nextjs/routing)
│
├── lib/
│ └── api/
│ └── courses.ts ← getCourseBySlug(), getAllCourses()
│
└── types/
└── course.ts ← Course interface
10. Practice Task
Task: Build a dynamic blog post page.
Requirements:
- Create
app/blog/[slug]/page.tsx - Create a mock
getPostBySlug(slug)function that returns post data - Display the post title, content, author, and published date
- Use
generateMetadatafor SEO - Handle 404s with
notFound() - Create
app/blog/page.tsxthat lists all posts linking to their dynamic routes
Starter data:
1const posts = [ 2 { 3 slug: "getting-started-with-nextjs", 4 title: "Getting Started with Next.js", 5 content: "Next.js is a React framework...", 6 author: "John Doe", 7 date: "2026-08-15", 8 }, 9 { 10 slug: "mastering-typescript", 11 title: "Mastering TypeScript", 12 content: "TypeScript adds type safety...", 13 author: "Jane Smith", 14 date: "2026-08-10", 15 }, 16];
11. Mini Project: Course Detail Page
Build a complete course detail page for Tech3Space with these features:
File Structure
app/courses/[slug]/
├── page.tsx ← Main course page
├── loading.tsx ← Skeleton loader
├── error.tsx ← Error boundary
├── not-found.tsx ← 404 page
└── layout.tsx ← Course detail layout
page.tsx
1import { notFound } from "next/navigation"; 2import { getCourseBySlug } from "@/lib/api/courses"; 3import { CourseHero } from "@/components/courses/CourseHero"; 4import { CourseCurriculum } from "@/components/courses/CourseCurriculum"; 5import { CourseInstructor } from "@/components/courses/CourseInstructor"; 6 7interface Props { 8 params: Promise<{ slug: string }>; 9} 10 11export async function generateMetadata({ params }: Props) { 12 const { slug } = await params; 13 const course = await getCourseBySlug(slug); 14 15 return { 16 title: course ? `${course.title} | Tech3Space` : "Course Not Found", 17 description: course?.description, 18 }; 19} 20 21export default async function CoursePage({ params }: Props) { 22 const { slug } = await params; 23 const course = await getCourseBySlug(slug); 24 25 if (!course) { 26 notFound(); 27 } 28 29 return ( 30 <div className="space-y-16"> 31 <CourseHero course={course} /> 32 <CourseCurriculum topics={course.topics} /> 33 <CourseInstructor instructor={course.instructor} /> 34 </div> 35 ); 36}
loading.tsx
1export default function CourseLoading() { 2 return ( 3 <div className="max-w-5xl mx-auto px-4 py-12 animate-pulse"> 4 <div className="h-8 bg-gray-200 rounded w-2/3 mb-4" /> 5 <div className="h-4 bg-gray-200 rounded w-full mb-2" /> 6 <div className="h-4 bg-gray-200 rounded w-5/6 mb-8" /> 7 <div className="h-64 bg-gray-200 rounded-xl" /> 8 </div> 9 ); 10}
not-found.tsx
1import Link from "next/link"; 2 3export default function CourseNotFound() { 4 return ( 5 <div className="text-center py-20"> 6 <h1 className="text-6xl font-bold text-gray-200 mb-4">404</h1> 7 <h2 className="text-2xl font-semibold mb-4">Course Not Found</h2> 8 <p className="text-gray-600 mb-8"> 9 The course you're looking for doesn't exist or has been moved. 10 </p> 11 <Link 12 href="/courses" 13 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700" 14 > 15 Browse All Courses → 16 </Link> 17 </div> 18 ); 19}
Summary
| Concept | Syntax | Example URL |
|---|---|---|
| Static Route | folder/page.tsx | /about |
| Dynamic Route | [slug]/page.tsx | /courses/python |
| Multiple Dynamic | [a]/[b]/page.tsx | /courses/101/lessons/3 |
| Catch-All | [...slug]/page.tsx | /docs/a/b/c |
| Optional Catch-All | [[...slug]]/page.tsx | /docs or /docs/a |
| 404 Handling | notFound() | Invalid slug |
| Dynamic SEO | generateMetadata() | Unique title per page |
| Pre-rendering | generateStaticParams() | Build-time generation |
What's Next?
In Module 7, we'll explore Nested Dynamic Routes — the architecture that powers Tech3Space's topic-level learning content with URLs like /courses/nextjs/server-components and /courses/python/functions.