Next.js course Β· Module 2: Routing and Layouts

Nested Paths and Catch-All Parameters

15 min read
In this lesson10

In the Quantum Metropolis, the transportation system enables residents to reach every corner of the city, and even unexplored territories. Special Q-Hopper vehicles can collect multiple travel destinations and visit them all in sequence, dynamically adapting to passenger needs. This flexibility is key to the functioning of such an advanced urban structure.

In the world of Next.js 16, similar flexibility is provided by advanced routing features such as nested paths and catch-all parameters. They allow handling complex URL structures and dynamic route generation based on data.

Dynamic Nested Paths

In Next.js 16, we can create nested dynamic paths by combining static and dynamic segments at different levels of the hierarchy:

1app/
2  β”œβ”€β”€ destinations/              # /destinations
3  β”‚   β”œβ”€β”€ page.tsx               # Destinations list page
4  β”‚   └── [destinationId]/       # Dynamic segment for destination ID
5  β”‚       β”œβ”€β”€ page.tsx           # /destinations/:destinationId
6  β”‚       └── attractions/       # /destinations/:destinationId/attractions
7  β”‚           β”œβ”€β”€ page.tsx       # Attractions page for the destination
8  β”‚           └── [attractionId] # Nested dynamic segment
9  β”‚               └── page.tsx   # /destinations/:destinationId/attractions/:attractionId

In this structure, we have two levels of dynamic segments: [destinationId] and [attractionId]. This allows creating paths such as /destinations/mars/attractions/olympus-mons.

Implementing Dynamic Nested Paths

Here's an example of how to implement a nested paths system for our Quantum Voyages application:

1// app/destinations/[destinationId]/page.tsx
2import { getDestinationById } from '@/lib/api';
3import { DestinationDetails } from '@/components/destinations/DestinationDetails';
4import { AttractionsPreview } from '@/components/destinations/AttractionsPreview';
5import Link from 'next/link';
6import { notFound } from 'next/navigation';
7
8// Generating static parameters for the most popular destinations
9export async function generateStaticParams() {
10  // Fetch the most popular destinations for prerendering
11  const destinations = await getPopularDestinations();
12
13  return destinations.map((destination) => ({
14    destinationId: destination.id,
15  }));
16}
17
18export default async function DestinationPage({ params }: { params: Promise<{ destinationId: string }> }) {
19  const { destinationId } = await params;
20  const destination = await getDestinationById(destinationId);
21
22  // If the destination doesn't exist, show the 404 page
23  if (!destination) {
24    notFound();
25  }
26
27  return (
28    <div className="container mx-auto px-4 py-12">
29      <h1 className="text-4xl font-bold mb-6">{destination.name}</h1>
30
31      <DestinationDetails destination={destination} />
32
33      <section className="mt-12">
34        <div className="flex justify-between items-center mb-6">
35          <h2 className="text-2xl font-semibold">Popular Attractions</h2>
36          <Link
37            href={`/destinations/${destinationId}/attractions`}
38            className="text-indigo-600 hover:underline"
39          >
40            See all attractions
41          </Link>
42        </div>
43
44        <AttractionsPreview destinationId={destinationId} />
45      </section>
46    </div>
47  );
48}
1// app/destinations/[destinationId]/attractions/page.tsx
2import { getDestinationById, getAttractionsForDestination } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function DestinationAttractionsPage({ params }: { params: Promise<{ destinationId: string }> }) {
8  const { destinationId } = await params;
9  const destination = await getDestinationById(destinationId);
10
11  if (!destination) {
12    notFound();
13  }
14
15  const attractions = await getAttractionsForDestination(destinationId);
16
17  return (
18    <div className="container mx-auto px-4 py-12">
19      <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
20        <Link href="/destinations" className="hover:text-indigo-600">Destinations</Link>
21        <span>/</span>
22        <Link href={`/destinations/${destinationId}`} className="hover:text-indigo-600">
23          {destination.name}
24        </Link>
25        <span>/</span>
26        <span>Attractions</span>
27      </div>
28
29      <h1 className="text-3xl font-bold mb-2">Attractions at {destination.name}</h1>
30      <p className="text-gray-600 mb-8">{destination.description}</p>
31
32      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
33        {attractions.map((attraction) => (
34          <Link
35            key={attraction.id}
36            href={`/destinations/${destinationId}/attractions/${attraction.id}`}
37            className="block group"
38          >
39            <div className="rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow">
40              <div className="relative h-48">
41                <Image
42                  src={attraction.imageUrl}
43                  alt={attraction.name}
44                  fill
45                  className="object-cover transition-transform group-hover:scale-105"
46                />
47              </div>
48              <div className="p-4">
49                <h2 className="text-xl font-semibold mb-2">{attraction.name}</h2>
50                <p className="text-gray-600 line-clamp-2">{attraction.shortDescription}</p>
51              </div>
52            </div>
53          </Link>
54        ))}
55      </div>
56    </div>
57  );
58}
1// app/destinations/[destinationId]/attractions/[attractionId]/page.tsx
2import { getDestinationById, getAttractionById } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function AttractionPage({
8  params
9}: {
10  params: Promise<{ destinationId: string; attractionId: string }>
11}) {
12  const { destinationId, attractionId } = await params;
13
14  const [destination, attraction] = await Promise.all([
15    getDestinationById(destinationId),
16    getAttractionById(destinationId, attractionId)
17  ]);
18
19  if (!destination || !attraction) {
20    notFound();
21  }
22
23  return (
24    <div className="container mx-auto px-4 py-12">
25      <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
26        <Link href="/destinations" className="hover:text-indigo-600">Destinations</Link>
27        <span>/</span>
28        <Link href={`/destinations/${destinationId}`} className="hover:text-indigo-600">
29          {destination.name}
30        </Link>
31        <span>/</span>
32        <Link href={`/destinations/${destinationId}/attractions`} className="hover:text-indigo-600">
33          Attractions
34        </Link>
35        <span>/</span>
36        <span>{attraction.name}</span>
37      </div>
38
39      <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
40        <div className="relative h-[400px] lg:h-[600px] rounded-lg overflow-hidden">
41          <Image
42            src={attraction.imageUrl}
43            alt={attraction.name}
44            fill
45            className="object-cover"
46            preload
47          />
48        </div>
49
50        <div>
51          <h1 className="text-4xl font-bold mb-4">{attraction.name}</h1>
52          <div className="flex items-center mb-6">
53            <span className="px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm">
54              {attraction.category}
55            </span>
56            <span className="mx-4 text-gray-300">|</span>
57            <div className="flex items-center text-amber-500">
58              {/* Rating stars */}
59              {Array.from({ length: 5 }).map((_, i) => (
60                <svg key={i} xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 ${i < attraction.rating ? 'fill-current' : 'text-gray-300'}`} viewBox="0 0 20 20" fill="currentColor">
61                  <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
62                </svg>
63              ))}
64              <span className="ml-2 text-gray-700">{attraction.rating.toFixed(1)}</span>
65            </div>
66          </div>
67
68          <p className="text-gray-700 mb-6">{attraction.description}</p>
69
70          <div className="space-y-4 mb-8">
71            <div className="flex">
72              <div className="w-32 font-semibold">Location:</div>
73              <div>{attraction.location}</div>
74            </div>
75            <div className="flex">
76              <div className="w-32 font-semibold">Visiting Cost:</div>
77              <div>{attraction.price} credits</div>
78            </div>
79            <div className="flex">
80              <div className="w-32 font-semibold">Visiting Time:</div>
81              <div>{attraction.duration}</div>
82            </div>
83          </div>
84
85          <button className="w-full py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
86            Book a Visit
87          </button>
88        </div>
89      </div>
90    </div>
91  );
92}

Catch-All Parameters

In the Quantum Metropolis, Q-Route technology enables traveling to any number of places based on a single route code. Similarly, in Next.js 16, catch-all parameters enable handling paths with any number of segments.

Catch-All Parameter Syntax

Catch-all parameters are denoted by three dots before the parameter name in square brackets:

1[...param]

This notation allows matching any number of URL segments, which are passed as an array to the component.

Directory Structure

1app/
2  β”œβ”€β”€ shop/                   # /shop
3  β”‚   └── [...categories]/    # /shop/category1
4  β”‚       └── page.tsx        # /shop/category1/category2/category3/etc.

In this example, the path /shop/clothing/men/shirts will be matched by [...categories] and the categories parameter will contain the array ['clothing', 'men', 'shirts'].

Implementation for Quantum Voyages

Implementation of a product category system with catch-all parameters:

1// app/shop/[...categories]/page.tsx
2import { fetchCategorizedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6
7export default async function CategoriesPage({ params }: { params: Promise<{ categories: string[] }> }) {
8  const { categories } = await params;
9
10  // Fetching products based on all provided categories
11  const { products, availableFilters } = await fetchCategorizedProducts(categories);
12
13  // Constructing the title based on the last category
14  const currentCategory = categories[categories.length - 1];
15  const formattedCategoryName = currentCategory
16    .split('-')
17    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
18    .join(' ');
19
20  return (
21    <div className="container mx-auto px-4 py-12">
22      <Breadcrumbs categories={categories} />
23
24      <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
25
26      <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
27        <div className="lg:col-span-1">
28          <CategoryFilters
29            categories={categories}
30            availableFilters={availableFilters}
31          />
32        </div>
33
34        <div className="lg:col-span-3">
35          <ProductGrid products={products} />
36        </div>
37      </div>
38    </div>
39  );
40}

Helper Components

1// components/shop/Breadcrumbs.tsx
2import Link from 'next/link';
3
4export function Breadcrumbs({ categories }: { categories: string[] }) {
5  return (
6    <div className="flex items-center flex-wrap gap-2 mb-8 text-sm text-gray-500">
7      <Link href="/shop" className="hover:text-indigo-600">
8        Shop
9      </Link>
10
11      {categories.map((category, index) => {
12        // Building the URL path for this category
13        const url = `/shop/${categories.slice(0, index + 1).join('/')}`;
14        const isLast = index === categories.length - 1;
15
16        const formattedName = category
17          .split('-')
18          .map(word => word.charAt(0).toUpperCase() + word.slice(1))
19          .join(' ');
20
21        return (
22          <div key={category} className="flex items-center">
23            <span className="mx-2">/</span>
24            {isLast ? (
25              <span>{formattedName}</span>
26            ) : (
27              <Link href={url} className="hover:text-indigo-600">
28                {formattedName}
29              </Link>
30            )}
31          </div>
32        );
33      })}
34    </div>
35  );
36}

Optional Catch-All Parameters

In Next.js 16, we can also define optional catch-all parameters using double square brackets:

1[[...param]]

The main difference is that an optional catch-all parameter will also match the path without any segments. For example, app/shop/[[...categories]]/page.tsx will match:

  • /shop (here categories is missing, i.e. undefined, which is why the code below uses || [])
  • /shop/clothing (categories = ['clothing'])
  • /shop/clothing/men/shirts (categories = ['clothing', 'men', 'shirts'])

Implementing a Shop with Optional Catch-All Parameters

1// app/shop/[[...categories]]/page.tsx
2import { fetchCategorizedProducts, fetchFeaturedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6import { FeaturedProducts } from '@/components/shop/FeaturedProducts';
7import { CategoryPreview } from '@/components/shop/CategoryPreview';
8
9export default async function ShopPage({ params }: { params: Promise<{ categories?: string[] }> }) {
10  const categories = (await params).categories || [];
11  const isRootShopPage = categories.length === 0;
12
13  if (isRootShopPage) {
14    // We're on the main shop page
15    const featuredProducts = await fetchFeaturedProducts();
16    const topCategories = await fetchTopCategories();
17
18    return (
19      <div className="container mx-auto px-4 py-12">
20        <h1 className="text-4xl font-bold mb-12">Shop Quantum Voyages</h1>
21
22        <section className="mb-16">
23          <h2 className="text-2xl font-semibold mb-8">Featured Products</h2>
24          <FeaturedProducts products={featuredProducts} />
25        </section>
26
27        <section>
28          <h2 className="text-2xl font-semibold mb-8">Browse Categories</h2>
29          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
30            {topCategories.map(category => (
31              <CategoryPreview key={category.id} category={category} />
32            ))}
33          </div>
34        </section>
35      </div>
36    );
37  } else {
38    // We're on a category page
39    const { products, availableFilters } = await fetchCategorizedProducts(categories);
40
41    // Constructing the title based on the last category
42    const currentCategory = categories[categories.length - 1];
43    const formattedCategoryName = currentCategory
44      .split('-')
45      .map(word => word.charAt(0).toUpperCase() + word.slice(1))
46      .join(' ');
47
48    return (
49      <div className="container mx-auto px-4 py-12">
50        <Breadcrumbs categories={categories} />
51
52        <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
53
54        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
55          <div className="lg:col-span-1">
56            <CategoryFilters
57              categories={categories}
58              availableFilters={availableFilters}
59            />
60          </div>
61
62          <div className="lg:col-span-3">
63            <ProductGrid products={products} />
64          </div>
65        </div>
66      </div>
67    );
68  }
69}

Accessing Parameters in Components

In Next.js 16, we can access path parameters in several ways:

In Server Components

1// Server Component
2export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
3  // Using (await params).productId
4  return <div>Product ID: {(await params).productId}</div>;
5}

In Client Components

In client-side components, we can use the useParams hook from next/navigation:

1'use client';
2
3import { useParams } from 'next/navigation';
4
5export default function ClientProductDetails() {
6  const params = useParams();
7  const productId = params.productId;
8
9  return <div>Viewing product: {productId}</div>;
10}

In Generation Functions

Next.js 16 offers functions for generating static parameters and metadata:

1// Generating static parameters
2export async function generateStaticParams() {
3  const products = await fetchTopProducts();
4
5  return products.map((product) => ({
6    productId: product.id,
7  }));
8}
9
10// Generating metadata
11export async function generateMetadata({ params }: { params: Promise<{ productId: string }> }) {
12  const product = await fetchProductById((await params).productId);
13
14  return {
15    title: product.name,
16    description: product.description,
17    openGraph: {
18      images: [{ url: product.imageUrl }],
19    },
20  };
21}

Parameter Validation and Handling

When working with dynamic parameters, it's important to validate them:

1export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
2  // Check if productId is a valid UUID
3  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((await params).productId)) {
4    // Throw an error that will be caught by the nearest error.js
5    throw new Error('Invalid product ID format');
6  }
7
8  const product = await fetchProductById((await params).productId);
9
10  // If the product doesn't exist, show the 404 page
11  if (!product) {
12    notFound();
13  }
14
15  return <ProductDetails product={product} />;
16}

Error and Missing Resource Handling

Next.js 16 enables elegant handling of situations when a resource is not found or an error occurs:

Error Handling with error.js

The error.tsx file must be a Client Component (the 'use client' directive): an error boundary in React relies on lifecycle methods that exist only in the browser, and the button with reset() needs a click handler.

1// app/products/[productId]/error.tsx
2'use client';
3
4import { useEffect } from 'react';
5
6export default function ProductError({
7  error,
8  reset,
9}: {
10  error: Error & { digest?: string };
11  reset: () => void;
12}) {
13  useEffect(() => {
14    // You can log the error to an external service
15    console.error(error);
16  }, [error]);
17
18  return (
19    <div className="container mx-auto px-4 py-16 text-center">
20      <h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
21      <p className="mb-8 text-gray-600">{error.message}</p>
22      <button
23        onClick={reset}
24        className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
25      >
26        Try again
27      </button>
28    </div>
29  );
30}

Missing Resource Handling with not-found.js

1// app/products/[productId]/not-found.tsx
2import Link from 'next/link';
3
4export default function ProductNotFound() {
5  return (
6    <div className="container mx-auto px-4 py-16 text-center">
7      <h2 className="text-3xl font-bold mb-4">Product Not Found</h2>
8      <p className="mb-8 text-gray-600">
9        Sorry, we couldn't find the product you're looking for.
10      </p>
11      <Link
12        href="/products"
13        className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
14      >
15        Back to all products
16      </Link>
17    </div>
18  );
19}

Example: Advanced Documentation System

In the Quantum Metropolis, an interactive documentation system allows navigating through complex information hierarchies. Let's implement a similar system in Next.js 16:

1app/
2  β”œβ”€β”€ docs/
3  β”‚   └── [[...slug]]/     # Optional catch-all
4  β”‚       └── page.tsx     # /docs, /docs/guides, /docs/guides/routing, etc.
1// app/docs/[[...slug]]/page.tsx
2import { fetchDocumentBySlug, fetchDocCategories } from '@/lib/api/documentation';
3import { DocSidebar } from '@/components/docs/DocSidebar';
4import { DocContent } from '@/components/docs/DocContent';
5import { DocBreadcrumbs } from '@/components/docs/DocBreadcrumbs';
6import { DocPagination } from '@/components/docs/DocPagination';
7import { notFound } from 'next/navigation';
8
9export default async function DocPage({ params }: { params: Promise<{ slug?: string[] }> }) {
10  const slug = (await params).slug || [];
11  const slugPath = slug.join('/');
12
13  // Fetch documentation categories for the sidebar menu
14  const categories = await fetchDocCategories();
15
16  // If no path is provided, show the main documentation page
17  if (slug.length === 0) {
18    return (
19      <div className="container mx-auto px-4 py-12">
20        <div className="flex flex-col lg:flex-row gap-8">
21          <DocSidebar categories={categories} currentSlug="" />
22
23          <main className="flex-1">
24            <h1 className="text-4xl font-bold mb-8">Quantum Voyages Documentation</h1>
25
26            <div className="prose prose-indigo max-w-none">
27              <p>
28                Welcome to the Quantum Voyages documentation center. Here you'll find all the information
29                needed to plan and take a space journey, from basic guides
30                to advanced technical instructions.
31              </p>
32
33              <h2>Popular Sections</h2>
34
35              <div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose">
36                {categories.map(category => (
37                  <div key={category.slug} className="border rounded-lg p-6 hover:shadow-md transition-shadow">
38                    <h3 className="text-xl font-semibold mb-2">{category.title}</h3>
39                    <p className="text-gray-600 mb-4">{category.description}</p>
40                    <a href={`/docs/${category.slug}`} className="text-indigo-600 hover:underline">
41                      Overviewaj {category.title.toLowerCase()} β†’
42                    </a>
43                  </div>
44                ))}
45              </div>
46            </div>
47          </main>
48        </div>
49      </div>
50    );
51  }
52
53  // Fetch document based on the path
54  const document = await fetchDocumentBySlug(slugPath);
55
56  // If the document doesn't exist, show the 404 page
57  if (!document) {
58    notFound();
59  }
60
61  // Fetch previous and next documents for navigation
62  const { previousDoc, nextDoc } = await fetchAdjacentDocs(slugPath);
63
64  return (
65    <div className="container mx-auto px-4 py-12">
66      <DocBreadcrumbs slug={slug} />
67
68      <div className="flex flex-col lg:flex-row gap-8">
69        <DocSidebar categories={categories} currentSlug={slugPath} />
70
71        <main className="flex-1">
72          <h1 className="text-4xl font-bold mb-8">{document.title}</h1>
73
74          <DocContent content={document.content} />
75
76          <DocPagination previousDoc={previousDoc} nextDoc={nextDoc} />
77        </main>
78      </div>
79    </div>
80  );
81}
82
83// Generating static parameters for popular documentation paths
84export async function generateStaticParams() {
85  const popularDocs = await fetchPopularDocs();
86
87  return popularDocs.map(doc => ({
88    slug: doc.slug.split('/'),
89  }));
90}

Combining Catch-All Parameters with Other Features

Next.js 16 allows combining catch-all parameters with other advanced routing features:

1. Grouping Routes with Catch-All Parameters

1app/
2  β”œβ”€β”€ (dashboard)/
3  β”‚   └── admin/
4  β”‚       └── users/
5  β”‚           └── [...roles]/    # /admin/users/admin/editor/etc.
6  β”‚               └── page.tsx

2. Intercepting Routes with Catch-All Parameters

1// app/(shop)/products/@modal/(.)[...categories]/page.tsx
2// Intercepts /products/category1/category2 as a modal during navigation from /products.
3// A catch-all segment must be the last one in the path: [productId] cannot come after [...categories].

3. Parallel Routes with Catch-All Parameters

1app/
2  β”œβ”€β”€ trips/
3  β”‚   β”œβ”€β”€ page.tsx              # /trips
4  β”‚   β”œβ”€β”€ @filters/
5  β”‚   β”‚   β”œβ”€β”€ default.tsx       # Required in Next.js 16
6  β”‚   β”‚   └── [...params]/      # Parallel route for filters
7  β”‚   β”‚       └── page.tsx
8  β”‚   └── @map/
9  β”‚       β”œβ”€β”€ default.tsx       # Required in Next.js 16
10  β”‚       └── [...location]/    # Parallel route for map
11  β”‚           └── page.tsx

Advanced Navigation Variants with Parameters

1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useState } from 'react';
5
6export function CategoryNavigator({ categories, subcategories }) {
7  const router = useRouter();
8  const [selectedCategoryId, setSelectedCategoryId] = useState('');
9
10  const handleCategoryChange = (categoryId) => {
11    setSelectedCategoryId(categoryId);
12    const selectedCategory = categories.find(c => c.id === categoryId);
13    router.push(`/shop/${selectedCategory.slug}`);
14  };
15
16  const handleSubcategoryChange = (categoryId, subcategoryId) => {
17    const selectedCategory = categories.find(c => c.id === categoryId);
18    const selectedSubcategory = subcategories.find(sc => sc.id === subcategoryId);
19    router.push(`/shop/${selectedCategory.slug}/${selectedSubcategory.slug}`);
20  };
21
22  return (
23    <div className="space-y-4">
24      <div>
25        <label className="block mb-2 text-sm font-medium">Category</label>
26        <select
27          className="w-full p-2 border rounded"
28          onChange={(e) => handleCategoryChange(e.target.value)}
29        >
30          <option value="">Select a category</option>
31          {categories.map(category => (
32            <option key={category.id} value={category.id}>
33              {category.name}
34            </option>
35          ))}
36        </select>
37      </div>
38
39      <div>
40        <label className="block mb-2 text-sm font-medium">Subcategory</label>
41        <select
42          className="w-full p-2 border rounded"
43          onChange={(e) => handleSubcategoryChange(selectedCategoryId, e.target.value)}
44        >
45          <option value="">Select a subcategory</option>
46          {/* Subcategory options */}
47        </select>
48      </div>
49    </div>
50  );
51}

Advanced Search with Catch-All Parameters

1// app/search/[[...query]]/page.tsx
2import { searchProducts } from '@/lib/api/search';
3
4export default async function SearchPage({
5  params,
6  searchParams,
7}: {
8  params: Promise<{ query?: string[] }>;
9  searchParams: Promise<{ category?: string; sort?: string; filters?: string }>;
10}) {
11  const query = (await params).query || [];
12  const searchTerm = query.join(' ');
13
14  // Fetching query parameters
15  const { category, sort, filters } = await searchParams;
16
17  // Performing the search
18  const results = await searchProducts({
19    searchTerm,
20    category,
21    sort,
22    filters: filters ? JSON.parse(filters) : undefined
23  });
24
25  return (
26    <div className="container mx-auto px-4 py-12">
27      <h1 className="text-3xl font-bold mb-8">
28        {searchTerm
29          ? `Wyniki wyszukiwania dla: "${searchTerm}"`
30          : 'Product Search'
31        }
32      </h1>
33
34      {/* Search filters */}
35      <SearchFilters
36        initialCategory={category}
37        initialSort={sort}
38        initialFilters={filters ? JSON.parse(filters) : {}}
39      />
40
41      {/* Search results */}
42      <SearchResults results={results} />
43    </div>
44  );
45}

Summary

Nested dynamic paths and catch-all parameters in Next.js 16 offer an extremely flexible routing system that can handle virtually any navigation scenario in modern web applications:

  1. Nested dynamic parameters enable creating complex hierarchical URL structures, such as /destinations/:destinationId/attractions/:attractionId.

  2. Catch-all parameters ([...param]) allow matching any number of URL segments, which is ideal for documentation systems, product categories, and flexible search interfaces.

  3. Optional catch-all parameters ([[...param]]) additionally handle the case of no segments, allowing the use of the same component for the base path and all its subpaths.

  4. Error and missing resource handling through error.js and not-found.js files ensures an elegant user experience even when problems occur.

  5. Parameter validation prevents errors and ensures application security.

By combining these techniques with features like static parameter generation (generateStaticParams), we can create efficient, dynamic, and easy-to-maintain navigation systems for applications of any scale and complexity.

In the next lesson you will see how layouts and loading.tsx files form the permanent infrastructure of pages.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Route Groups in App Router are marked using:

  2. 2. Catch-all segments in App Router are created using:

Hands-on tasks in the game

  • Code editor

    Create the file error.js: start with the 'use client' directive (error.js must be a Client Component), then write the default-exported ErrorPage({ error, reset }) component. Show a friendly error message, e.g. an h2 heading β€œSomething went wrong!”, and a β€œTry again” button that calls reset() when clicked.

  • Vertical ordering

    Arrange the steps for creating a dynamic route in App Router

  • Click in order

    Arrange the default export syntax of a page component in Next.js

  • Code editor

    Create a Client Component Counter (default export): the file starts with 'use client', you create the count state with useState(0), display it in a p paragraph, and a β€œ+1” button increases the counter by 1 on every click.

Useful articles