Build an Ecommerce Store with Google Sheets + Next.js (Complete Guide)
Tutorial

Build an Ecommerce Store with Google Sheets + Next.js (Complete Guide)

Pikebyte.dev
January 22, 2026
14 min read
#ecommerce#google-sheets#nextjs#shopify-alternative#no-code-ecommerce

Build an Ecommerce Store with Google Sheets + Next.js (Complete Guide)

Shopify's $29/month plan too expensive for your side project? Tired of monthly fees eating into your profit margins?

There's a better way. Build a fully functional ecommerce store using Google Sheets as your product database and Next.js as your storefront. Total cost: $0-20/month.

In this guide, you'll learn how to build a complete ecommerce system that your non-technical team members can manage.


🤔 Why Google Sheets + Next.js for Ecommerce?

Free Forever (or nearly free)

No monthly subscription. Hosting on Vercel is free. Google Sheets is free.

Non-Tech Team Can Manage Products

Your marketing team updates the spreadsheet, the website updates instantly.

Full Customization

Build exactly what you want. No limitations from Shopify's templates.


🏗️ Architecture: How It All Works Together

Your Customers
    ↓
Next.js Storefront
    ↓
SheetToAPI (REST API)
    ↓
Google Sheets (Product Database)
    ↓
Stripe (Payment Processing)
    ↓
Zapier (Order Notifications & Fulfillment)

Each layer explained:

  • Next.js: Your customer-facing storefront (fast, modern, SEO-friendly)
  • SheetToAPI: Converts your spreadsheet into a REST API
  • Google Sheets: Your product catalog (updated by your team)
  • Stripe: Handles payments securely
  • Zapier: Automates order notifications and fulfillment

📋 Step 1: Structure Your Google Sheet

Create a new Google Sheet with the following columns:

| id | name            | description                | price | category    | image_url | in_stock | inventory |
|----|-----------------|----------------------------|-------|-------------|-----------|----------|-----------|
| 1  | Wireless Mouse  | Ergonomic, 2.4GHz USB      | 29.99 | electronics | https://... | true     | 50        |
| 2  | USB-C Cable     | 2m, 60W charging           | 12.99 | accessories | https://... | true     | 120       |
| 3  | Laptop Stand    | Aluminum, adjustable angle | 49.99 | accessories | https://... | false    | 0         |

Column Reference:

  • id: Unique identifier (use =ROW()-1 formula to auto-increment)
  • name: Product title
  • description: Short product description
  • price: Retail price (numbers only, no $ symbol)
  • category: Product category for filtering (electronics, accessories, etc.)
  • image_url: Full URL to product image (host on Cloudinary or similar)
  • in_stock: TRUE or FALSE
  • inventory: Quantity available

Pro Tips:

  • Keep descriptions under 200 characters for better mobile display
  • Use high-quality images (800×600px minimum, optimized)
  • Add more columns as needed: sku, supplier, cost, rating, reviews

🔌 Step 2: Connect to SheetToAPI

  1. Go to sheettoapi.com
  2. Click "Create New API"
  3. Paste your Google Sheet URL
  4. Give it a name: ecommerce-store
  5. Copy your endpoint and API key
  6. Save to .env.local:
NEXT_PUBLIC_SHEETTOAPI_ENDPOINT=https://api.sheettoapi.com/api/v1/data/ecommerce-store
NEXT_PUBLIC_SHEETTOAPI_KEY=your_api_key_here
STRIPE_SECRET_KEY=sk_test_your_stripe_key
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_key

💻 Step 3: Build Your Next.js Store

Create a New Next.js Project:

npx create-next-app@latest ecommerce-store
cd ecommerce-store
npm install @stripe/react-stripe-js stripe

Create API Helper (lib/api.ts):

const API_ENDPOINT = process.env.NEXT_PUBLIC_SHEETTOAPI_ENDPOINT!;
const API_KEY = process.env.NEXT_PUBLIC_SHEETTOAPI_KEY!;

export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  category: string;
  image_url: string;
  in_stock: boolean;
  inventory: number;
}

export async function getProducts(category?: string): Promise<Product[]> {
  let url = API_ENDPOINT;
  if (category) {
    url += `?category=${encodeURIComponent(category)}`;
  }

  const response = await fetch(url, {
    headers: { 'X-API-Key': API_KEY }
  });

  if (!response.ok) throw new Error('Failed to fetch products');
  const data = await response.json();
  return data.rows;
}

export async function getProduct(id: string): Promise<Product> {
  const products = await getProducts();
  const product = products.find(p => p.id === id);
  if (!product) throw new Error('Product not found');
  return product;
}

Product Listing Page (app/products/page.tsx):

'use client';

import { useEffect, useState } from 'react';
import { getProducts, Product } from '@/lib/api';
import Image from 'next/image';
import Link from 'next/link';

export default function ProductsPage() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const data = await getProducts();
        setProducts(data);
      } catch (error) {
        console.error('Failed to load products:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, []);

  if (loading) return <div className="text-center p-8">Loading products...</div>;

  return (
    <div className="max-w-6xl mx-auto p-8">
      <h1 className="text-4xl font-bold mb-8">Our Products</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {products.filter(p => p.in_stock).map(product => (
          <Link href={`/products/${product.id}`} key={product.id}>
            <div className="border rounded-lg overflow-hidden hover:shadow-lg transition">
              <Image 
                src={product.image_url} 
                alt={product.name}
                width={300}
                height={300}
                className="w-full h-64 object-cover"
              />
              <div className="p-4">
                <h3 className="font-bold text-lg">{product.name}</h3>
                <p className="text-gray-600 text-sm mb-2">{product.description}</p>
                <div className="flex justify-between items-center">
                  <span className="text-2xl font-bold">${product.price}</span>
                  <span className="text-green-600 text-sm">In Stock</span>
                </div>
              </div>
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

Product Detail & Checkout (app/products/[id]/page.tsx):

'use client';

import { useEffect, useState } from 'react';
import { getProduct, Product } from '@/lib/api';
import { loadStripe } from '@stripe/stripe-js';
import Image from 'next/image';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

export default function ProductPage({ params }: { params: { id: string } }) {
  const [product, setProduct] = useState<Product | null>(null);
  const [quantity, setQuantity] = useState(1);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchProduct = async () => {
      try {
        const data = await getProduct(params.id);
        setProduct(data);
      } catch (error) {
        console.error('Failed to load product:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchProduct();
  }, [params.id]);

  const handleCheckout = async () => {
    const stripe = await stripePromise;
    if (!product || !stripe) return;

    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        product: product,
        quantity: quantity
      })
    });

    const { sessionId } = await response.json();
    await stripe.redirectToCheckout({ sessionId });
  };

  if (loading) return <div className="text-center p-8">Loading...</div>;
  if (!product) return <div className="text-center p-8">Product not found</div>;

  return (
    <div className="max-w-4xl mx-auto p-8 grid grid-cols-2 gap-8">
      <div>
        <Image 
          src={product.image_url} 
          alt={product.name}
          width={500}
          height={500}
          className="w-full rounded-lg"
        />
      </div>
      
      <div>
        <h1 className="text-4xl font-bold mb-4">{product.name}</h1>
        <p className="text-gray-600 mb-6">{product.description}</p>
        <p className="text-5xl font-bold text-green-600 mb-6">${product.price}</p>
        
        <div className="mb-6">
          <label className="block mb-2">Quantity:</label>
          <input 
            type="number" 
            min="1" 
            max={product.inventory}
            value={quantity}
            onChange={(e) => setQuantity(parseInt(e.target.value))}
            className="border px-4 py-2 rounded w-20"
          />
        </div>
        
        <button 
          onClick={handleCheckout}
          disabled={!product.in_stock}
          className="w-full bg-blue-600 text-white py-4 rounded-lg font-bold text-lg hover:bg-blue-700 disabled:bg-gray-400"
        >
          {product.in_stock ? 'Add to Cart' : 'Out of Stock'}
        </button>
      </div>
    </div>
  );
}

💳 Step 4: Set Up Stripe Payments

Create Checkout API Route (app/api/checkout/route.ts):

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

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const { product, quantity } = await request.json();

  try {
    const session = await stripe.checkout.sessions.create({
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: product.name,
              images: [product.image_url],
            },
            unit_amount: Math.round(product.price * 100),
          },
          quantity: quantity,
        },
      ],
      mode: 'payment',
      success_url: `${request.headers.get('origin')}/success`,
      cancel_url: `${request.headers.get('origin')}/products`,
    });

    return NextResponse.json({ sessionId: session.id });
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

🚀 Step 5: Deploy to Vercel

npm install -g vercel
vercel
  1. Connect your GitHub repo
  2. Add environment variables in Vercel dashboard
  3. Deploy!

Your ecommerce store is now live.


🔄 Step 6: Automate Order Notifications with Zapier

Create a Zap that sends you an email every time someone places an order:

  1. Trigger: Stripe payment successful
  2. Action 1: Create row in Google Sheets (log order)
  3. Action 2: Send you an email notification
  4. Action 3: Send customer a receipt email

📊 Real-World Pricing Comparison

Building a small ecommerce store:

ComponentCost/Month
Shopify$29-299
Google Sheets + Next.js$0-20
Stripe fees2.9% + $0.30/transaction (same for all)

On $10k/month revenue:

  • Shopify: $29/month subscription + $300 transaction fees = $329
  • Google Sheets + Next.js: $0 + $300 transaction fees = $300
  • Savings: $29/month ($348/year)

Plus unlimited design freedom and no rate limits.


⚡ Advanced Features (via Zapier + Integrations)

  1. Inventory Sync: Use Zapier to update Sheets when stock runs low
  2. Email Receipts: Send automated order confirmations
  3. SMS Notifications: Notify customers when items ship
  4. Abandoned Cart: Remind customers about unpurchased items
  5. Analytics Dashboard: Sync orders to Google Sheets for reporting

🎯 When to Scale Up

This setup handles:

  • ✅ Up to 50k products
  • ✅ 1-5k orders/month
  • ✅ Single-currency sales

When you need:

  • ❌ Inventory management at scale
  • ❌ Shipping integrations (FedEx, UPS)
  • ❌ Subscription products
  • ❌ Multiple currencies

→ Migrate to Shopify, WooCommerce, or Medusa


🏆 Conclusion

You now have a complete, free ecommerce store that:

  • Costs nearly nothing to run
  • Can be managed by non-technical team members
  • Offers unlimited design flexibility
  • Handles hundreds of products and orders

Get started today: Create a Google Sheet, connect it to SheetToAPI, and deploy your Next.js store to Vercel in under 30 minutes.

Your side project just became a real business. 🚀

P

Pikebyte.dev

Expert contributor sharing insights and best practices for building with SheetToAPI.

Ready to build?

Start using SheetToAPI today and turn your spreadsheets into powerful APIs.

Explore Documentation