Skip to content

ISR Mini Project — Build a Complete ISR-Powered Application

DodaTech Updated 2026-06-28 6 min read

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

Build a complete ISR-powered e-commerce product catalog combining tiered revalidation, webhook updates, monitoring, and deployment.

What You'll Learn

By the end of this project, you'll build a production-ready ISR application that demonstrates all the concepts covered in this course: tiered revalidation, webhook-triggered updates, database integration, Caching, fallback strategies, and monitoring.

Why It Matters

Theory without practice doesn't stick. This project brings together every ISR technique you've learned, giving you a real-world portfolio piece that showcases your incremental static regeneration skills.

Real-World Use

An e-commerce startup launches a product catalog with 10,000 products using ISR. Popular products pre-build at deploy time, others generate on first visit via fallback. Prices update within 60 seconds, inventory within 30 seconds, and CMS-driven content updates instantly via Webhooks.

Project Architecture

graph TD
    A[ISR Product Catalog] --> B[Next.js SSG + ISR]
    B --> C[Product pages
tiered ISR] B --> D[Category pages
ISR + listing] B --> E[Homepage
ISR 60s] A --> F[Database
PostgreSQL] A --> G[CMS
Contentful] A --> H[Monitoring
Dashboard] C --> I[fallback: blocking
for new products] D --> J[Pagination
20 per page] F --> K[Product data
prices, stock] G --> L[Content
descriptions, images] H --> M[Revalidation logs
success rate] style A fill:#4a90d9,color:#fff style B fill:#e67e22,color:#fff style H fill:#27ae60,color:#fff

Project Setup

// Package.json dependencies
{
    "name": "isr-product-catalog",
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "db:seed": "node scripts/seed-db.js",
        "monitor": "node scripts/monitor-cli.js"
    },
    "dependencies": {
        "next": "^14.0.0",
        "react": "^18.0.0",
        "pg": "^8.11.0",
        "contentful": "^10.0.0"
    }
}

Product Page with Tiered ISR

// pages/products/[id].js — Tiered ISR product page
export default function ProductPage({ product, tier, generatedAt }) {
    if (!product) {
        return (
            <div className="not-found">
                <h1>Product Not Found</h1>
                <a href="/products">Browse all products</a>
            </div>
        );
    }

    return (
        <div className="product-page">
            <nav className="breadcrumb">
                <a href="/">Home</a> /
                <a href={`/categories/${product.category.slug}`}>
                    {product.category.name}
                </a> /
                <span>{product.name}</span>
            </nav>

            <div className="product-grid">
                <div className="product-image">
                    <img src={product.imageUrl} alt={product.name} />
                </div>

                <div className="product-details">
                    <h1>{product.name}</h1>
                    <p className="price">
                        ${product.price.toFixed(2)}
                        {product.originalPrice && (
                            <span className="original-price">
                                ${product.originalPrice.toFixed(2)}
                            </span>
                        )}
                    </p>

                    <div className={`stock ${product.inStock ? 'in-stock' : 'out-of-stock'}`}>
                        {product.inStock
                            ? `In Stock (${product.stockCount} available)`
                            : 'Out of Stock'}
                    </div>

                    <p className="description">{product.description}</p>

                    <div className="tags">
                        {product.tags.map(tag => (
                            <span key={tag} className="tag">{tag}</span>
                        ))}
                    </div>
                </div>
            </div>

            <footer className="page-meta">
                <GenerationTimestamp
                    generatedAt={generatedAt}
                    revalidate={tier.revalidate}
                />
                <span className="tier-badge">{tier.label}</span>
            </footer>
        </div>
    );
}

export async function getStaticPaths() {
    // Pre-build popular products
    const popular = await fetchPopularProducts(100);
    const paths = popular.map(p => ({
        params: { id: String(p.id) }
    }));

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

export async function getStaticProps({ params }) {
    const product = await fetchProduct(params.id);
    if (!product) return { notFound: true };

    const tier = getTier(product);

    return {
        props: {
            product,
            tier,
            generatedAt: Date.now()
        },
        revalidate: tier.revalidate
    };
}

function getTier(product) {
    if (product.isFlashSale) return { revalidate: 10, label: 'Flash Sale' };
    if (product.stockCount < 10) return { revalidate: 30, label: 'Low Stock' };
    if (product.isNew) return { revalidate: 60, label: 'New Arrival' };
    return { revalidate: 300, label: 'Standard' };
}

Category Listing with Pagination

// pages/products/page/[page].js — Paginated product listing
export default function ProductListing({ products, pagination, generatedAt }) {
    return (
        <div>
            <h1>All Products</h1>
            <p className="count">{pagination.total} products</p>

            <div className="product-grid">
                {products.map(product => (
                    <a key={product.id} href={`/products/${product.id}`}
                       className="product-card">
                        <img src={product.thumbnailUrl} alt={product.name} />
                        <h3>{product.name}</h3>
                        <p className="price">${product.price.toFixed(2)}</p>
                    </a>
                ))}
            </div>

            <nav className="pagination">
                {pagination.page > 1 && (
                    <a href={`/products/page/${pagination.page - 1}`}>Previous</a>
                )}
                <span>Page {pagination.page} of {pagination.totalPages}</span>
                {pagination.page < pagination.totalPages && (
                    <a href={`/products/page/${pagination.page + 1}`}>Next</a>
                )}
            </nav>

            <footer>
                <GenerationTimestamp generatedAt={generatedAt} revalidate={30} />
            </footer>
        </div>
    );
}

const PRODUCTS_PER_PAGE = 20;

export async function getStaticPaths() {
    const total = await fetchProductCount();
    const totalPages = Math.ceil(total / PRODUCTS_PER_PAGE);

    const paths = Array.from({ length: totalPages }, (_, i) => ({
        params: { page: String(i + 1) }
    }));

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

export async function getStaticProps({ params }) {
    const page = parseInt(params.page);
    const { products, total } = await fetchProductsPage(page, PRODUCTS_PER_PAGE);

    return {
        props: {
            products,
            pagination: {
                page,
                total,
                totalPages: Math.ceil(total / PRODUCTS_PER_PAGE)
            },
            generatedAt: Date.now()
        },
        revalidate: 30
    };
}

Revalidation & Monitoring

// pages/api/monitor.js — Monitoring dashboard API
import { isrMonitor } from '../../lib/isr-monitor';

export default async function handler(req, res) {
    const summary = isrMonitor.getSummary();

    res.json({
        status: 'ok',
        metrics: summary,
        timestamp: Date.now()
    });
}

// lib/isr-monitor.js — Simplified monitor for the project
class ProjectISRMonitor {
    constructor() {
        this.logs = [];
        this.startTime = Date.now();
    }

    log(event) {
        this.logs.push({
            ...event,
            timestamp: Date.now()
        });
    }

    getStats() {
        const revalidations = this.logs.filter(l => l.type === 'revalidation');
        const successes = revalidations.filter(l => l.success);

        return {
            totalRevalidations: revalidations.length,
            successRate: revalidations.length > 0
                ? (successes.length / revalidations.length * 100).toFixed(1)
                : 100,
            avgDuration: revalidations.length > 0
                ? (revalidations.reduce((s, l) => s + l.duration, 0) / revalidations.length).toFixed(0)
                : 0,
            uptime: Math.floor((Date.now() - this.startTime) / 1000),
            recentErrors: revalidations.filter(l => !l.success).slice(-10)
        };
    }
}

export const monitor = new ProjectISRMonitor();

Common Mistakes

  1. Not testing ISR behavior during traffic spikes. ISR serves cached pages during spikes, but the revalidation server still needs capacity. Load test revalidation endpoints.
  2. Forgetting to handle the case where both time-based and on-demand revalidation trigger simultaneously. They work fine together, but monitor for duplicate work.
  3. Not documenting the ISR configuration. Team members need to know revalidate values, fallback strategies, and webhook endpoints. Document in the project README.
  4. Skipping the monitoring dashboard. Without monitoring, you won't know if ISR is working correctly. Build a simple dashboard from day one.
  5. Deploying without testing fallback behavior. Test that fallback: 'blocking' correctly generates and caches new pages. Test with a page that doesn't exist.

Practice Questions

  1. How do you combine tiered revalidation with fallback strategies?
  2. How do you implement paginated ISR listings?
  3. What monitoring metrics are essential for an ISR production application?
  4. How do you handle CMS webhooks alongside time-based revalidation?
  5. How do you test ISR before deploying to production?

Challenge: Extend the ISR product catalog with: search functionality (Lunr.js with ISR), related products section (ISR with relationships), an admin dashboard showing ISR health, automated testing of revalidation behavior, and deployment to Vercel with monitoring.

FAQ

How do I ensure ISR pages are available during deployment?

Pre-build popular pages in getStaticPaths. Use fallback: 'blocking' for others. The first request after deployment triggers generation, subsequent requests are cached.

Should I use ISR for the entire site or only specific pages?

Use ISR only for pages that need periodic updates. Marketing pages (SSG), product pages (ISR), dashboards (SSR). Mix strategies per page type.

How does ISR handle 50,000 products?

Pre-build the top 1,000 popular products. Use fallback: 'blocking' for the rest. Stagger revalidation times. Monitor cache hit rates and revalidation load.

Can I A/B test with ISR?

Not directly, since ISR caches per URL. Implement A/B testing via client-side experimentation tools or use SSR for test pages.

How do I roll back ISR changes?

Deploy the previous version. ISR cache persists across deployments. Use on-demand revalidation to refresh pages with the old code.

Mini Project

Build and deploy a complete ISR-powered e-commerce catalog: set up PostgreSQL with 100+ products, implement tiered ISR (flash sale: 10s, low stock: 30s, standard: 300s), add category pages with paginated ISR, configure a monitoring dashboard, and deploy to Vercel.

What's Next

Congratulations on completing the ISR course! You've built a production-ready ISR application. Now explore the next frontend topic: Lazy Loading to improve performance by deferring non-critical resources.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro