Lesson 20 +10 XP

Project: E-Commerce Store with ISR & Route Handlers

Project: E-Commerce Store with ISR & Route Handlers

In this project, you will build a scalable E-Commerce Product Catalog with shopping cart API Route Handlers, tag-based revalidation (revalidateTag), and static metadata.

Project Architecture

  • app/api/cart/route.ts: API Route Handler for cart item retrieval and addition.
  • app/products/[id]/page.tsx: ISR product details page with tag-based caching.
  • app/actions/cart.ts: Server Action invoking revalidateTag('cart').

1. Cart API Route Handler (app/api/cart/route.ts)

import { NextRequest, NextResponse } from 'next/server';

let cartItems: { productId: string; quantity: number }[] = [];

export async function GET() {
  return NextResponse.json({ items: cartItems });
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (!body.productId) {
    return NextResponse.json({ error: 'Missing productId' }, { status: 400 });
  }

  cartItems.push({ productId: body.productId, quantity: body.quantity || 1 });

  return NextResponse.json({ success: true, items: cartItems }, { status: 201 });
}

2. Product Details Page with ISR Tags (app/products/[id]/page.tsx)

import { Metadata } from 'next';
import { addToCartAction } from '@/app/actions/cart';

type Props = {
  params: Promise<{ id: string }>;
};

async function getProduct(id: string) {
  // Fetch with 1-hour ISR revalidation + tag!
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 3600, tags: ['products', `product-${id}`] },
  });
  return res.json();
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params;
  const product = await getProduct(id);

  return {
    title: `${product.title} - Buy Online`,
    description: product.description,
  };
}

export default async function ProductDetailsPage({ params }: Props) {
  const { id } = await params;
  const product = await getProduct(id);

  return (
    <main className="max-w-4xl mx-auto p-8 grid grid-cols-2 gap-8">
      <img src={product.image} alt={product.title} className="rounded shadow" />
      <div>
        <h1 className="text-3xl font-bold">{product.title}</h1>
        <p className="text-2xl text-green-600 font-bold mt-2">${product.price}</p>
        <p className="mt-4 text-gray-600">{product.description}</p>

        <form action={addToCartAction} className="mt-6">
          <input type="hidden" name="productId" value={id} />
          <button type="submit" className="bg-black text-white px-6 py-3 rounded font-bold">
            Add to Cart
          </button>
        </form>
      </div>
    </main>
  );
}

3. Server Action Invalidating Cart Cache (app/actions/cart.ts)

'use server';

import { revalidateTag } from 'next/cache';

export async function addToCartAction(formData: FormData) {
  const productId = formData.get('productId') as string;

  // Perform backend cart database update...
  console.log('Added product to cart:', productId);

  // Invalidate cart tags programmatically across the entire site!
  revalidateTag('cart');
}

TL;DR

  • Tag fetch calls in product pages with next: { tags: ['products'] }.
  • Use revalidateTag('cart') in Server Actions to refresh cart counters instantly.
  • Export generateMetadata to optimize SEO title and meta tags per product dynamically.