Gen-Endpoint
Back to Blog

Stateful Shopping Cart Prototyping: Add to Cart, Quantities, and Checkout Flow

By Kunal Burangi•Published on November 1, 2026•
E-Commerce & Frontend
#E-Commerce
#React
#State Management
#REST API
#Optimistic UI

Stateful Shopping Cart Prototyping: Add to Cart, Quantities, and Checkout Flow

Building an e-commerce shopping cart is one of the most demanding frontend tasks. A cart is not a simple read-only data view; it is a highly stateful, event-driven system requiring:

  • Optimistic UI Updates: Instant user feedback when clicking "Add to Cart" or incrementing items.
  • Race Condition Prevention: Debouncing rapid quantity clicks so out-of-order network responses don't corrupt the cart.
  • Dynamic Price Reconciliation: Calculating line-item totals, discount codes, tiered shipping thresholds, and tax estimates.
  • State Persistence: Preserving items when a user navigates between product catalog pages and checkout screens.

In this guide, we review the architecture of modern shopping carts, implement an optimistic cart manager in React, and connect it with Gen-Endpoint's free Cart API and Products API.


Why Stateless Mocks Break Down in E-Commerce

Most free mock APIs are completely stateless. When you send a POST /cart request to add an item, a stateless mock returns { "success": true }, but when your checkout page later requests GET /cart, it returns an empty array or a hardcoded default list.

This forces frontend developers to maintain complex in-memory mock hacks in their local components, which hides bugs like:

  • Inventory caps (e.g., maximum 5 items per customer).
  • Calculation discrepancies between client math and server roundoffs.
  • Coupon code validations and discount expirations.

Using Gen-Endpoint's stateful Cart API, your items persist throughout your session, allowing true end-to-end testing from catalog to checkout.


The E-Commerce Cart Data Contract

Before building the UI, define the data contract:

// types/cart.ts
export interface Product {
  id: string;
  title: string;
  price: number;
  image: string;
  stock: number;
}

export interface CartItem {
  id: string;
  productId: string;
  title: string;
  unitPrice: number;
  quantity: number;
  itemTotal: number;
  image: string;
}

export interface CartSummary {
  items: CartItem[];
  subtotal: number;
  discount: number;
  shipping: number;
  tax: number;
  grandTotal: number;
}

Building an Optimistic Shopping Cart in React

When an online shopper clicks + to increase an item quantity, waiting 600ms for a network round-trip makes the interface feel sluggish and unresponsive.

Optimistic updates solve this: We update the React state immediately so the counter advances instantly, send the mutation to the backend in the background, and roll back to the previous state only if the request fails:

// hooks/useCart.ts
'use client';

import { useState, useEffect, useCallback } from 'react';
import type { CartItem, CartSummary } from '@/types/cart';

export function useCart() {
  const [cart, setCart] = useState<CartSummary>({
    items: [],
    subtotal: 0,
    discount: 0,
    shipping: 0,
    tax: 0,
    grandTotal: 0,
  });
  const [isLoading, setIsLoading] = useState<boolean>(true);

  // Recalculate financial totals
  const recalculateSummary = (items: CartItem[]): CartSummary => {
    const subtotal = items.reduce((acc, item) => acc + item.unitPrice * item.quantity, 0);
    const shipping = subtotal > 50 || subtotal === 0 ? 0 : 5.99; // Free shipping over $50
    const tax = Number((subtotal * 0.08).toFixed(2)); // 8% sales tax estimate
    const discount = 0;
    const grandTotal = Number((subtotal + shipping + tax - discount).toFixed(2));

    return { items, subtotal, discount, shipping, tax, grandTotal };
  };

  // Fetch initial cart state from Gen-Endpoint
  useEffect(() => {
    async function loadCart() {
      try {
        const res = await fetch('https://gen-endpoint.com/api/cart');
        if (res.ok) {
          const data = await res.json();
          const items = Array.isArray(data.items) ? data.items : [];
          setCart(recalculateSummary(items));
        }
      } catch (err) {
        console.error('Failed to load initial cart:', err);
      } finally {
        setIsLoading(false);
      }
    }
    loadCart();
  }, []);

  // Optimistic quantity update
  const updateQuantity = useCallback(async (itemId: string, newQuantity: number) => {
    if (newQuantity < 1) return;

    // 1. Snapshot previous state for rollback
    const previousCart = cart;

    // 2. Compute optimistic items
    const updatedItems = cart.items.map((item) =>
      item.id === itemId
        ? { ...item, quantity: newQuantity, itemTotal: item.unitPrice * newQuantity }
        : item
    );

    // 3. Immediately update UI
    setCart(recalculateSummary(updatedItems));

    // 4. Send background sync to API
    try {
      const response = await fetch(`https://gen-endpoint.com/api/cart/${itemId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ quantity: newQuantity }),
      });

      if (!response.ok) {
        throw new Error('Server rejected quantity update');
      }
    } catch (err) {
      console.error('Mutation failed, rolling back cart:', err);
      // Rollback on failure
      setCart(previousCart);
      alert('Could not update quantity. Please check your internet connection.');
    }
  }, [cart]);

  // Remove item from cart
  const removeItem = useCallback(async (itemId: string) => {
    const previousCart = cart;
    const updatedItems = cart.items.filter((item) => item.id !== itemId);
    setCart(recalculateSummary(updatedItems));

    try {
      const res = await fetch(`https://gen-endpoint.com/api/cart/${itemId}`, {
        method: 'DELETE',
      });
      if (!res.ok) throw new Error('Deletion failed');
    } catch (err) {
      setCart(previousCart);
    }
  }, [cart]);

  return { cart, isLoading, updateQuantity, removeItem };
}

The Cart Checkout Drawer Component

Here is a full cart drawer component with responsive layout and total breakdowns:

// components/CartDrawer.tsx
'use client';

import React from 'react';
import { useCart } from '@/hooks/useCart';

export function CartDrawer() {
  const { cart, isLoading, updateQuantity, removeItem } = useCart();

  if (isLoading) {
    return <div className="p-8 text-center text-sm">Loading your cart...</div>;
  }

  if (cart.items.length === 0) {
    return (
      <div className="p-12 text-center space-y-3">
        <h3 className="text-lg font-bold">Your Cart is Empty</h3>
        <p className="text-sm text-muted-foreground">Add products to begin checkout.</p>
      </div>
    );
  }

  return (
    <div className="max-w-md mx-auto p-6 bg-card border rounded-2xl shadow-lg space-y-6">
      <h2 className="text-xl font-bold tracking-tight">Your Order ({cart.items.length} items)</h2>

      {/* Item List */}
      <div className="divide-y divide-border">
        {cart.items.map((item) => (
          <div key={item.id} className="py-4 flex items-center justify-between gap-4">
            <div>
              <h4 className="font-semibold text-sm">{item.title}</h4>
              <p className="text-xs text-muted-foreground">${item.unitPrice.toFixed(2)} each</p>
            </div>

            {/* Quantity Controls */}
            <div className="flex items-center gap-2">
              <button
                onClick={() => updateQuantity(item.id, item.quantity - 1)}
                className="w-7 h-7 rounded border flex items-center justify-center font-bold text-sm hover:bg-muted"
              >
                -
              </button>
              <span className="w-6 text-center text-sm font-semibold">{item.quantity}</span>
              <button
                onClick={() => updateQuantity(item.id, item.quantity + 1)}
                className="w-7 h-7 rounded border flex items-center justify-center font-bold text-sm hover:bg-muted"
              >
                +
              </button>
            </div>

            <div className="text-right">
              <p className="font-bold text-sm">${item.itemTotal.toFixed(2)}</p>
              <button
                onClick={() => removeItem(item.id)}
                className="text-xs text-destructive hover:underline"
              >
                Remove
              </button>
            </div>
          </div>
        ))}
      </div>

      {/* Financial Summary */}
      <div className="p-4 bg-muted/40 rounded-xl space-y-2 text-sm">
        <div className="flex justify-between text-muted-foreground">
          <span>Subtotal</span>
          <span>${cart.subtotal.toFixed(2)}</span>
        </div>
        <div className="flex justify-between text-muted-foreground">
          <span>Shipping</span>
          <span>{cart.shipping === 0 ? 'FREE' : `$${cart.shipping.toFixed(2)}`}</span>
        </div>
        <div className="flex justify-between text-muted-foreground">
          <span>Estimated Tax</span>
          <span>${cart.tax.toFixed(2)}</span>
        </div>
        <div className="border-t pt-2 flex justify-between font-bold text-base text-foreground">
          <span>Total</span>
          <span>${cart.grandTotal.toFixed(2)}</span>
        </div>
      </div>

      <button
        onClick={() => alert('Proceeding to checkout with total: $' + cart.grandTotal)}
        className="w-full py-3 rounded-xl bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors shadow-sm"
      >
        Proceed to Checkout
      </button>
    </div>
  );
}

Summary & Next Steps

Prototyping e-commerce flows requires realistic data persistence and optimistic mutation updates. With Gen-Endpoint's Cart API and Products API, you can stress-test edge cases, shipping calculations, and checkout transitions without writing a single line of backend database code.

Free Developer Tooling

Ready to Test These Endpoints?

Access 23+ free mock REST APIs, test file uploads, and generate Next.js API handlers with zero signup.