Skip to main content

Version 1: Foundation

Focus: API Routes, Dynamic Routing, Cart State


Overview

Version 1 establishes the foundational architecture: connecting frontend to backend via API routes, implementing dynamic routing for product pages, and creating global cart state management.

Goal: Browser → API → Data Response → Render

1A - API Routes

Objective

Create the first API endpoint and connect frontend to backend.

Implementation

API Endpoint: /api/products/route.ts

// Initial version: hardcoded data
export async function GET() {
const products = [
{ id: 1, name: "Phone Mount", price: 29.99 },
{ id: 2, name: "Car Mount", price: 39.99 },
// ...
];

return NextResponse.json({ products });
}

Frontend: /products/page.tsx

export default function ProductsPage() {
const [products, setProducts] = useState([]);

useEffect(() => {
fetch("/api/products")
.then((res) => res.json())
.then((data) => setProducts(data.products));
}, []);

return (
<div className="grid grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}

Key Learning

  • API Routes: Next.js convention for backend endpoints
  • fetch(): Browser's native HTTP client
  • useState + useEffect: React's data fetching pattern

Verification

✅ Visit /products
✅ See product grid rendered
✅ Network tab shows /api/products request

1B - Dynamic Routing

Objective

Implement product detail pages with URL parameters.

Implementation

Dynamic Route: /products/[id]/page.tsx

export default function ProductDetailPage() {
const { id } = useParams();
const [product, setProduct] = useState(null);

useEffect(() => {
fetch(`/api/products/${id}`)
.then((res) => res.json())
.then((data) => setProduct(data.product));
}, [id]);

if (!product) return <div>Loading...</div>;

return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}

API Endpoint: /api/products/[id]/route.ts

export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
const product = products.find((p) => p.id === parseInt(params.id));

if (!product) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}

return NextResponse.json({ product });
}

Key Learning

ConceptPractical Meaning
[id] folderDynamic URL segments
useParams()Read URL parameters in client components
Route handlersBackend logic per route

Verification

✅ Click product card → navigates to /products/1
✅ Detail page shows correct product
✅ "Loading..." flash confirms async fetch

1C - Cart Context

Objective

Global cart state accessible from any page.

Implementation

Context Provider: /context/CartContext.tsx

interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}

interface CartContextType {
cart: CartItem[];
addToCart: (product: Product) => void;
removeFromCart: (productId: number) => void;
updateQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
}

const CartContext = createContext<CartContextType | null>(null);

export function CartProvider({ children }: { children: React.ReactNode }) {
const [cart, setCart] = useState<CartItem[]>([]);

const addToCart = (product: Product) => {
setCart((prev) => {
const existing = prev.find((item) => item.id === product.id);
if (existing) {
return prev.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};

const removeFromCart = (productId: number) => {
setCart((prev) => prev.filter((item) => item.id !== productId));
};

const updateQuantity = (productId: number, quantity: number) => {
setCart((prev) =>
prev.map((item) => (item.id === productId ? { ...item, quantity } : item))
);
};

const clearCart = () => setCart([]);

return (
<CartContext.Provider
value={{ cart, addToCart, removeFromCart, updateQuantity, clearCart }}
>
{children}
</CartContext.Provider>
);
}

export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error("useCart must be used within CartProvider");
return context;
}

Layout Integration: /app/layout.tsx

export default function RootLayout({ children }) {
return (
<html>
<body>
<CartProvider>
<Navbar />
{children}
</CartProvider>
</body>
</html>
);
}

Usage in Components:

// Product detail page
const { addToCart } = useCart();
<button onClick={() => addToCart(product)}>Add to Cart</button>;

// Cart page
const { cart, removeFromCart, updateQuantity } = useCart();

Key Learning

ConceptPractical Meaning
Context APIShare state without prop drilling
Custom HookReusable logic encapsulation
Immutable UpdatessetCart(prev => [...]) pattern
Provider PatternWrap app to make state available

Verification

✅ Add product from /products/1
✅ Navigate to /cart → item appears
✅ Add same product → quantity increases
✅ Remove item → disappears from cart

Architecture Achieved

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ /products │────▶│ /api/ │────▶│ Hardcoded │
│ /products/ │ │ products │ │ JSON Data │
│ [id] │ │ /[id] │ │ │
└─────────────┘ └─────────────┘ └─────────────┘


┌─────────────┐
│ CartContext │
│ (React) │
└─────────────┘

Files Created

FilePurpose
src/app/api/products/route.tsProduct list endpoint
src/app/api/products/[id]/route.tsSingle product endpoint
src/app/products/page.tsxProduct listing page
src/app/products/[id]/page.tsxProduct detail page
src/app/cart/page.tsxCart page
src/app/context/CartContext.tsxGlobal cart state
src/app/layout.tsxRoot layout with providers

Limitations (To Address in v2)

LimitationImpact
Hardcoded dataNo real database
Client-side cartLost on refresh
No persistenceOrders not saved
No authenticationAnyone can checkout

Next Version Preview

Version 2.0 will replace hardcoded JSON with PostgreSQL database and add Stripe payment processing.