Skip to content

ISR Dynamic Routes — Using ISR with Dynamic Route Parameters

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about ISR Dynamic Routes. We cover key concepts, practical examples, and best practices to help you master this topic.

ISR with dynamic routes combines parameterized URLs with background revalidation, updating individual parameterized pages without full rebuilds.

What You'll Learn

By the end of this tutorial, you'll understand how ISR works with dynamic routes, how to combine getStaticPaths with ISR, handle many parameter combinations, and optimize dynamic ISR for large datasets.

Why It Matters

Dynamic routes like /products/[category]/[id] create a combinatorial explosion of possible paths. ISR lets you pre-build popular routes and generate others on demand, keeping build times manageable while serving all possible URLs.

Real-World Use

A travel site has routes like /destinations/[country]/[city]/[hotel]. With 100 countries, 1000 cities, and 10,000 hotels, pre-building all combinations is impossible. ISR pre-builds top hotels and generates others via fallback.

Dynamic ISR Architecture

graph TD
    A[getStaticPaths] --> B[Pre-build popular
parameter combinations] B --> C[Static HTML
for popular routes] A --> D[fallback: blocking
for unknown routes] D --> E[Request arrives
with new params] E --> F[Server renders page
with these params] F --> G[Cache result
with ISR revalidate] G --> H[Future requests
serve cached page] C --> I[All routes eventually
cached via ISR] H --> I style A fill:#4a90d9,color:#fff style D fill:#e67e22,color:#fff style I fill:#27ae60,color:#fff

Multiple Dynamic Parameters

// pages/[category]/[product].js — Multi-param dynamic ISR
export default function ProductPage({ product, category, params }) {
    return (
        <div>
            <nav className="breadcrumb">
                <a href="/">Home</a> /
                <a href={`/${category}`}>{category}</a> /
                <span>{product.name}</span>
            </nav>

            <h1>{product.name}</h1>
            <p>{product.description}</p>
            <p className="price">${product.price}</p>

            <small>
                Route: /{category}/{product.slug}
                <br />
                Generated: {new Date(params.generatedAt).toLocaleString()}
            </small>
        </div>
    );
}

export async function getStaticPaths() {
    // Pre-build top products from each category
    const categories = await fetch('https://api.example.com/categories')
        .then(r => r.json());

    const paths = [];

    for (const category of categories) {
        const products = await fetch(
            `https://api.example.com/${category.slug}/products`
        ).then(r => r.json());

        // Only pre-build top 5 per category
        products.slice(0, 5).forEach(product => {
            paths.push({
                params: {
                    category: category.slug,
                    product: product.slug
                }
            });
        });
    }

    return { paths, fallback: 'blocking' };
}

export async function getStaticProps({ params }) {
    const { category, product } = params;

    const res = await fetch(
        `https://api.example.com/${category}/${product}`
    );

    if (!res.ok) return { notFound: true };

    const productData = await res.json();

    return {
        props: {
            product: productData,
            category,
            params: { generatedAt: Date.now() }
        },
        revalidate: 60
    };
}

Catch-All Dynamic Routes

// pages/docs/[...slug].js — Catch-all dynamic ISR
export default function DocPage({ doc, params }) {
    return (
        <article>
            <h1>{doc.title}</h1>
            <div className="breadcrumbs">
                {params.slug.map((part, i) => (
                    <span key={i}>
                        {i > 0 && ' / '}
                        <a href={`/docs/${params.slug.slice(0, i + 1).join('/')}`}>
                            {part}
                        </a>
                    </span>
                ))}
            </div>
            <div>{doc.content}</div>
        </article>
    );
}

export async function getStaticPaths() {
    // Fetch all doc paths from the API
    const paths = await fetch('https://api.example.com/docs/paths')
        .then(r => r.json());

    return {
        paths: paths.map(p => ({
            params: { slug: p.split('/') }
        })),
        fallback: 'blocking'
    };
}

export async function getStaticProps({ params }) {
    const slugPath = params.slug.join('/');
    const res = await fetch(`https://api.example.com/docs/${slugPath}`);

    if (!res.ok) return { notFound: true };

    const doc = await res.json();

    return {
        props: { doc, params },
        revalidate: 300
    };
}

Optimized Path Generation for Large Datasets

// lib/optimized-paths.js — Batch path generation
const BATCH_SIZE = 100;
const CONCURRENCY = 5;

async function* generateAllPaths(totalCount) {
    for (let offset = 0; offset < totalCount; offset += BATCH_SIZE) {
        const batch = await fetchPathsBatch(offset, BATCH_SIZE);
        yield batch;
    }
}

async function fetchPathsBatch(offset, limit) {
    const res = await fetch(
        `https://api.example.com/products?offset=${offset}&limit=${limit}&fields=slug,category`
    );
    const data = await res.json();
    return data.items.map(item => ({
        params: {
            category: item.category.slug,
            product: item.slug
        }
    }));
}

// In getStaticPaths
export async function getStaticPaths() {
    // Get total count
    const { total } = await fetch('https://api.example.com/products/count')
        .then(r => r.json());

    const paths = [];
    const generator = generateAllPaths(total);

    for await (const batch of generator) {
        paths.push(...batch);

        // Yield to event loop every 500 paths
        if (paths.length % 500 === 0) {
            console.log(`Generated ${paths.length}/${total} paths...`);
            await new Promise(r => setTimeout(r, 0));
        }
    }

    console.log(`Total paths generated: ${paths.length}`);

    return {
        paths,
        fallback: 'blocking'
    };
}

Parameter Validation

// lib/validate-params.js — Dynamic route validation
const validCategories = new Set(['electronics', 'clothing', 'food', 'books']);
const MAX_SLUG_LENGTH = 100;

function validateProductParams(params) {
    const errors = [];

    // Validate category
    if (!params.category || typeof params.category !== 'string') {
        errors.push('Category is required and must be a string');
    } else if (!validCategories.has(params.category)) {
        errors.push(`Invalid category: ${params.category}`);
    }

    // Validate product slug
    if (!params.product || typeof params.product !== 'string') {
        errors.push('Product slug is required');
    } else if (params.product.length > MAX_SLUG_LENGTH) {
        errors.push('Product slug too long');
    } else if (!/^[a-z0-9-]+$/.test(params.product)) {
        errors.push('Product slug contains invalid characters');
    }

    return {
        valid: errors.length === 0,
        errors
    };
}

// Usage in getStaticProps
export async function getStaticProps({ params }) {
    const validation = validateProductParams(params);
    if (!validation.valid) {
        console.warn('Invalid params:', validation.errors);
        return { notFound: true };
    }

    // Proceed with data fetching
    // ...
}

Common Mistakes

  1. Pre-building too many dynamic route combinations. If you have 3 parameter types with 100 options each, that's 1M paths. Pre-build a subset and use fallback for the rest.
  2. Not validating dynamic parameters. Users can craft any URL. Validate parameters in getStaticProps to prevent unnecessary API calls and potential errors.
  3. Forgetting to include all required parameters. getStaticPaths must return params matching the dynamic segments. A missing parameter causes build errors.
  4. Using fallback: false with dynamic content. If new parameter combinations are added after deployment, fallback: false makes them inaccessible. Use 'blocking' for dynamic sets.
  5. Not Caching API responses in getStaticPaths. During development, every build re-fetches all paths. Cache the path list locally for faster iteration.

Practice Questions

  1. How do you combine ISR with dynamic route parameters?
  2. What is the challenge of using ISR with multiple dynamic parameters?
  3. How do you optimize path generation for large parameter spaces?
  4. How do you validate dynamic route parameters in getStaticProps?
  5. How does fallback: 'blocking' help with dynamic ISR routes?

Challenge: Build a multi-level dynamic ISR site with 3 parameter levels (category/subcategory/product), pre-build only the top 10% of combinations, use fallback: 'blocking' for the rest, implement parameter validation, and benchmark build time vs total routes available.

FAQ

Can ISR dynamic routes have optional parameters?

Yes. Use [[...slug]] for optional catch-all routes. The parameter may be undefined if no slug is provided. Handle this case in your code.

How many dynamic route combinations is too many?

There's no hard limit, but pre-building 100K+ paths increases build time significantly. Use fallback and pre-build only popular paths.

Can I mix ISR with client-side data fetching on dynamic routes?

Yes. Use ISR for initial HTML and SWR/React Query for client-side updates. This gives you fast initial load with fresh data after hydration.

How do dynamic ISR routes affect sitemap generation?

Generate sitemaps dynamically from your data source or use fallback: 'blocking' and let search engines discover pages through internal links.

Can I use regex patterns in dynamic routes?

No. Next.js dynamic routes use file-system based routing. Validate parameters in getStaticProps instead.

Mini Project

Create a multi-level directory site with ISR: implement routes like /[country]/[state]/[city], pre-build top 5 cities per state for popular countries, use fallback: 'blocking' for unknown combinations, validate geolocation parameters, and benchmark the caching behavior.

What's Next

Dynamic ISR is mastered. Now learn how to use ISR with Databases to fetch and cache database content in static pages.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro