ISR Mini Project — Build a Complete ISR-Powered Application
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
- Not testing ISR behavior during traffic spikes. ISR serves cached pages during spikes, but the revalidation server still needs capacity. Load test revalidation endpoints.
- Forgetting to handle the case where both time-based and on-demand revalidation trigger simultaneously. They work fine together, but monitor for duplicate work.
- Not documenting the ISR configuration. Team members need to know revalidate values, fallback strategies, and webhook endpoints. Document in the project README.
- Skipping the monitoring dashboard. Without monitoring, you won't know if ISR is working correctly. Build a simple dashboard from day one.
- 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
- How do you combine tiered revalidation with fallback strategies?
- How do you implement paginated ISR listings?
- What monitoring metrics are essential for an ISR production application?
- How do you handle CMS webhooks alongside time-based revalidation?
- 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
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