Building A Next Js Shopping Cart App

Create a performant, type-safe shopping cart with Next.js 15, Zustand state management, and TypeScript. A comprehensive guide to modern e-commerce cart development.

Why Choose Next.js and Zustand for Your Shopping Cart

A shopping cart is one of the most fundamental features in any e-commerce application. When building with Next.js, you have the advantage of server-side rendering, automatic code splitting, and excellent performance characteristics out of the box. Combined with Zustand, a lightweight state management library, you can create a shopping cart experience that feels instant, persists across sessions, and scales gracefully as your catalog grows.

Next.js provides the performance and SEO benefits that modern e-commerce demands, while Zustand offers a minimalist API that eliminates the boilerplate associated with larger state management solutions. Together, they represent the current state of the art in React e-commerce development.

If you're coming from a background using plain JavaScript or looking to understand the differences between TypeScript and JSDoc approaches, our guide on TypeScript vs JSDoc for JavaScript provides valuable context for making informed decisions about your project's type safety strategy.

The Zustand Advantage

Why developers choose Zustand for shopping cart state management

Minimal API Surface

No providers, no complex setup--just create a store and use it anywhere in your components with a simple hook.

TypeScript Native

Full type inference out of the box. Your cart state and actions are type-safe without extra configuration.

Built-in Persistence

The persist middleware makes localStorage synchronization trivial. Cart data survives page refreshes automatically.

DevTools Integration

Time-travel debugging and state inspection out of the box with Redux DevTools compatibility.

Setting Up Your Project

Before diving into the cart implementation, let's establish a solid foundation with proper project setup and dependencies. We'll use Next.js 15 with the App Router, TypeScript for type safety, and Tailwind CSS for styling.

Project Setup Commands
1npx create-next-app@latest ecommerce-store --typescript --tailwind --app2cd ecommerce-store3npm install zustand lucide-react
Recommended Project Structure
1src/2├── components/3│ ├── cart/4│ │ ├── CartSidebar.tsx5│ │ ├── CartItem.tsx6│ │ └── CartIcon.tsx7│ └── product/8│ ├── ProductCard.tsx9│ └── ProductGrid.tsx10├── stores/11│ └── useCartStore.ts12├── hooks/13│ └── useCart.ts14└── types/15 └── index.ts

Defining Your TypeScript Interfaces

Strong typing is essential for a maintainable shopping cart. Let's define the core interfaces that will underpin our entire implementation.

For teams working with existing JavaScript codebases, understanding the transition to TypeScript is crucial. The type safety we implement here prevents runtime errors and makes refactoring significantly safer. See our comprehensive comparison of TypeScript vs JSDoc approaches for guidance on choosing the right typing strategy for your project.

Product and Cart Item Types
1// src/types/index.ts2export interface Product {3 id: string;4 name: string;5 price: number;6 image: string;7 image_full?: string;8 category: string;9 description: string;10}11 12export interface CartItem {13 product: Product;14 quantity: number;15}
Cart State and Actions Interfaces
1// src/types/cart.ts2export interface CartState {3 items: CartItem[];4 isOpen: boolean;5}6 7export interface CartActions {8 addItem: (product: Product) => void;9 removeItem: (productId: string) => void;10 updateQuantity: (productId: string, quantity: number) => void;11 clearCart: () => void;12 toggleCart: () => void;13 getTotalItems: () => number;14 getTotalPrice: () => number;15}

Building the Cart Store with Zustand

The Zustand store is the heart of your shopping cart. It manages all cart state and provides the actions that components use to modify that state. The persist middleware ensures cart data survives page refreshes.

This approach to state management aligns with modern React patterns for building interactive user interfaces. For deeper insights into React state management patterns, explore our guide on React useActionState which covers another valuable addition to the React hooks ecosystem.

Complete Zustand Cart Store Implementation
1// src/stores/useCartStore.ts2import { create } from 'zustand';3import { persist, createJSONStorage } from 'zustand/middleware';4import { Product, CartItem, CartState, CartActions } from '@/types';5 6type CartStore = CartState & CartActions;7 8export const useCartStore = create<CartStore>()(9 persist(10 (set, get) => ({11 items: [],12 isOpen: false,13 14 addItem: (product: Product) => {15 const { items } = get();16 const existingItem = items.find(item => item.product.id === product.id);17 18 if (existingItem) {19 set({20 items: items.map(item =>21 item.product.id === product.id22 ? { ...item, quantity: item.quantity + 1}23 : item24 )25 });26 } else {27 set({ items: [...items, { product, quantity: 1 }] });28 }29 },30 31 removeItem: (productId: string) => {32 set({33 items: get().items.filter(item => item.product.id !== productId)34 });35 },36 37 updateQuantity: (productId: string, quantity: number) => {38 if (quantity <= 0) {39 get().removeItem(productId);40 return;41 }42 set({43 items: get().items.map(item =>44 item.product.id === productId45 ? { ...item, quantity }46 : item47 )48 });49 },50 51 clearCart: () => set({ items: [] }),52 toggleCart: () => set({ isOpen: !get().isOpen }),53 54 getTotalItems: () => {55 return get().items.reduce((total, item) => total + item.quantity, 0);56 },57 58 getTotalPrice: () => {59 return get().items.reduce(60 (total, item) => total + (item.product.price * item.quantity),61 062 );63 }64 }),65 {66 name: 'shopping-cart-storage',67 storage: createJSONStorage(() => localStorage),68 }69 )70);

Building the Product Display Components

Product cards are the entry point to your cart. They need to display product information clearly and provide easy access to add-to-cart functionality.

Product Card Component
1// src/components/product/ProductCard.tsx2import Image from 'next/image';3import { ShoppingCart } from 'lucide-react';4import { Product, useCartStore } from '@/stores/useCartStore';5 6interface ProductCardProps {7 product: Product;8}9 10export const ProductCard = ({ product }: ProductCardProps) => {11 const addItem = useCartStore(state => state.addItem);12 const toggleCart = useCartStore(state => state.toggleCart);13 14 const handleAddToCart = () => {15 addItem(product);16 toggleCart();17 };18 19 return (20 <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">21 <div className="relative h-48">22 <Image23 src={product.image}24 alt={product.name}25 fill26 className="object-cover"27 sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"28 />29 </div>30 <div className="p-4">31 <h3 className="text-lg font-semibold mb-2">{product.name}</h3>32 <p className="text-gray-600 mb-2">{product.category}</p>33 <div className="flex justify-between items-center">34 <span className="text-xl font-bold">${product.price}</span>35 <button36 onClick={handleAddToCart}37 className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"38 >39 <ShoppingCart size={18} />40 Add to Cart41 </button>42 </div>43 </div>44 </div>45 );46};

Implementing the Cart Sidebar

A slide-out cart sidebar provides a non-intrusive way for users to review and modify their cart while continuing to browse products.

Complete Cart Sidebar Component
1// src/components/cart/CartSidebar.tsx2import { X, Minus, Plus, Trash2 } from 'lucide-react';3import { useCartStore } from '@/stores/useCartStore';4import Image from 'next/image';5 6export const CartSidebar = () => {7 const { items, isOpen, removeItem, updateQuantity, toggleCart, getTotalPrice } = useCartStore();8 9 if (!isOpen) return null;10 11 return (12 <div className="fixed inset-0 z-50">13 <div className="absolute inset-0 bg-black/50" onClick={toggleCart} />14 <div className="absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl">15 <div className="flex flex-col h-full">16 <div className="flex items-center justify-between p-4 border-b">17 <h2 className="text-xl font-semibold">Shopping Cart</h2>18 <button onClick={toggleCart} className="p-2 hover:bg-gray-100 rounded-full">19 <X size={24} />20 </button>21 </div>22 23 <div className="flex-1 overflow-y-auto p-4">24 {items.length === 0 ? (25 <p className="text-center text-gray-500 mt-8">Your cart is empty</p>26 ) : (27 <ul className="space-y-4">28 {items.map(({ product, quantity }) => (29 <li key={product.id} className="flex gap-4 border-b pb-4">30 <div className="relative w-20 h-20">31 <Image32 src={product.image}33 alt={product.name}34 fill35 className="object-cover rounded"36 />37 </div>38 <div className="flex-1">39 <h3 className="font-medium">{product.name}</h3>40 <p className="text-gray-600">${product.price}</p>41 <div className="flex items-center gap-2 mt-2">42 <button43 onClick={() => updateQuantity(product.id, quantity - 1)}44 className="p-1 hover:bg-gray-100 rounded"45 >46 <Minus size={16} />47 </button>48 <span className="w-8 text-center">{quantity}</span>49 <button50 onClick={() => updateQuantity(product.id, quantity + 1)}51 className="p-1 hover:bg-gray-100 rounded"52 >53 <Plus size={16} />54 </button>55 </div>56 </div>57 <button58 onClick={() => removeItem(product.id)}59 className="text-red-500 hover:text-red-700"60 >61 <Trash2 size={20} />62 </button>63 </li>64 ))}65 </ul>66 )}67 </div>68 69 {items.length > 0 && (70 <div className="border-t p-4">71 <div className="flex justify-between text-lg font-semibold mb-4">72 <span>Total</span>73 <span>${getTotalPrice().toFixed(2)}</span>74 </div>75 <button className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700">76 Checkout77 </button>78 </div>79 )}80 </div>81 </div>82 </div>83 );84};

Best Practices for Shopping Cart Implementation

State Management Principles

  • Keep state normalized to prevent inconsistencies across the application
  • Use immutable updates to ensure predictable state transitions and enable time-travel debugging
  • Calculate derived data (totals, counts) rather than storing it to avoid synchronization issues
  • Consider optimistic updates for better perceived performance when adding items

User Experience Considerations

  • Provide immediate feedback when adding items through notifications or cart animation
  • Allow easy cart modification without requiring page navigation or reloads
  • Persist cart state so users don't lose selections when they close or refresh the browser
  • Handle edge cases like out-of-stock items gracefully with clear messaging

Accessibility

  • Ensure keyboard navigation works throughout the entire cart flow
  • Use proper ARIA labels and roles for screen reader compatibility
  • Provide screen reader announcements for cart changes and error states
  • Maintain focus management when opening and closing the cart sidebar

Frequently Asked Questions

Conclusion

Building a shopping cart with Next.js and Zustand combines the best of modern web development practices. Next.js provides excellent performance through server-side rendering and automatic optimization, while Zustand offers a simple, type-safe approach to state management.

The patterns covered in this guide--proper TypeScript typing, state persistence, component architecture, and performance optimization--provide a foundation that scales from small catalogs to large e-commerce platforms. As you extend your implementation, these same principles will guide you toward maintainable, performant code.

Remember that the shopping cart is often the final step before conversion. Every improvement you make to cart usability and performance directly impacts your bottom line. Invest in proper architecture now, and your future self (and your users) will thank you.

Need Help Building Your E-commerce Platform?

Our team specializes in modern web development with Next.js. We can help you build a high-performance e-commerce experience that converts.