Build an E-Commerce Store with AI: From Product Page to Checkout

You've built landing pages and to-do apps. Now it's time to build something that makes money. This tutorial walks you through creating a real online store — product grid, shopping cart, and Stripe Checkout — using AI to write the code. You'll have a working store that accepts payments by the end.

What You'll Build

A working e-commerce store with: a responsive product grid displaying items from a JSON file, a shopping cart with add/remove/quantity controls (saved to localStorage), and Stripe Checkout integration that handles real payments on a hosted, secure payment page. Stack: Next.js (or plain HTML/JS) + Stripe Checkout. No custom payment forms, no PCI compliance headaches. Time: ~3 hours. Cost: $0 to build, Stripe charges 2.9% + 30¢ per sale.

Why This Is the Project That Changes Everything

Every project you've built so far has been cool — but it hasn't made you money. A portfolio shows your work. A landing page collects emails. An e-commerce store puts money in your bank account.

Maybe you've got a friend who makes handmade candles and sells them through Instagram DMs. Maybe you've built a set of Notion templates and want to sell them for $19 each. Maybe a local business asked you to set up an online store. This is the project for all of those scenarios.

Here's the important thing: you don't need Shopify. Shopify charges $39/month minimum, takes a cut of every sale, and locks you into their ecosystem. With AI tools and Stripe, you can build a custom store for $0/month that you fully own and control. Stripe only charges when you actually make a sale.

The stack is deliberately simple:

  • Product data: A JSON file (no database needed to start)
  • Frontend: Next.js or plain HTML/JS for the product grid and cart
  • Cart state: localStorage (no backend needed for the cart itself)
  • Payments: Stripe Checkout (a hosted payment page — you never touch credit card numbers)
  • Hosting: Vercel, Netlify, or your own VPS

Let's build it.

Choose Your Stack: Next.js vs Plain HTML/JS

You have two solid options. Both work. Pick based on where you are:

Option A: Next.js (Recommended)

If you've used Next.js before or your AI tool scaffolds it easily, this is the better choice. Next.js gives you built-in API routes (so you can create the Stripe checkout endpoint without a separate server), file-based routing, and easy deployment to Vercel. This tutorial primarily follows this path.

Option B: Plain HTML/JS + a Small Server

If you want to keep things as bare-bones as possible, you can use plain HTML, CSS, and vanilla JavaScript for the frontend. The only catch: you'll still need a tiny server-side function to create Stripe Checkout sessions (Stripe requires this for security — you can't create sessions from the browser). A single Netlify Function or Vercel Serverless Function handles this.

This tutorial uses Next.js for the examples, but every concept translates directly to plain HTML/JS. The cart logic, product data structure, and Stripe integration work the same way.

Step 1: Set Up Your Project

AI Prompt

"Create a new Next.js project for an e-commerce store called 'The Little Shop.' Set up the following structure: (1) pages/index.js — the main store page, (2) pages/api/checkout.js — the Stripe checkout API route, (3) pages/success.js — order confirmation page, (4) pages/canceled.js — payment canceled page, (5) data/products.json — product catalog, (6) components/ProductCard.js, components/Cart.js, components/CartItem.js. Use the App Router if possible, but Pages Router is fine. Install stripe and @stripe/stripe-js as dependencies."

Your Product Data File

Start with a JSON file. This is your entire product catalog. No database, no CMS, no admin panel. When you have 5-20 products, this is all you need.

// data/products.json
[
  {
    "id": "candle-lavender",
    "name": "Lavender Dreams Candle",
    "description": "Hand-poured soy candle with real lavender buds. Burns for 40+ hours.",
    "price": 2400,
    "image": "/images/products/candle-lavender.webp",
    "category": "candles"
  },
  {
    "id": "candle-vanilla",
    "name": "Vanilla Bean Candle",
    "description": "Rich vanilla scent with coconut wax blend. 8oz jar.",
    "price": 1800,
    "image": "/images/products/candle-vanilla.webp",
    "category": "candles"
  },
  {
    "id": "bundle-starter",
    "name": "Starter Bundle (3 Candles)",
    "description": "Our three best sellers at a discount. Perfect gift set.",
    "price": 5400,
    "image": "/images/products/bundle-starter.webp",
    "category": "bundles"
  }
]

Important: Prices are in cents. $24.00 = 2400. This is how Stripe expects prices, and it avoids floating-point math issues. Every e-commerce system does this. When you display prices, divide by 100: (price / 100).toFixed(2).

Why JSON Instead of a Database?

Because you're starting. A JSON file means:

  • Zero infrastructure to manage
  • Version controlled with git (you can see every product change in your commit history)
  • Loads instantly (no database query, no network request)
  • Easy for AI to generate and modify

Move to a database when you need: more than ~50 products, real-time inventory tracking, non-technical people editing products, or search/filter across hundreds of items. Until then, JSON wins.

Step 2: Build the Product Grid

AI Prompt

"Build a responsive product grid component for my Next.js store. Requirements: (1) Import products from data/products.json, (2) Display each product as a card with image, name, description, price, and 'Add to Cart' button, (3) Grid: 3 columns on desktop, 2 on tablet, 1 on mobile, (4) Price displays as $XX.XX (divide cents by 100), (5) 'Add to Cart' button calls an addToCart(productId) function, (6) Use CSS Grid or Flexbox, (7) Cards should have subtle hover effect — slight lift and shadow. Style it modern and clean, dark theme with #0A0E1A background."

The Product Card Component

// components/ProductCard.js

export default function ProductCard({ product, onAddToCart }) {
  const displayPrice = (product.price / 100).toFixed(2)

  return (
    <div className="product-card">
      <div className="product-image">
        <img
          src={product.image}
          alt={product.name}
          loading="lazy"
          width={400}
          height={400}
        />
      </div>
      <div className="product-info">
        <h3 className="product-name">{product.name}</h3>
        <p className="product-description">{product.description}</p>
        <div className="product-footer">
          <span className="product-price">${displayPrice}</span>
          <button
            className="add-to-cart-btn"
            onClick={() => onAddToCart(product)}
          >
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  )
}

The Main Store Page

// pages/index.js
import { useState } from 'react'
import products from '../data/products.json'
import ProductCard from '../components/ProductCard'
import Cart from '../components/Cart'

export default function StorePage() {
  const [cart, setCart] = useState(() => {
    // Load cart from localStorage on first render
    if (typeof window !== 'undefined') {
      const saved = localStorage.getItem('cart')
      return saved ? JSON.parse(saved) : []
    }
    return []
  })

  const [isCartOpen, setIsCartOpen] = useState(false)

  function addToCart(product) {
    setCart(prev => {
      const existing = prev.find(item => item.id === product.id)
      let updated

      if (existing) {
        // Already in cart — increase quantity
        updated = prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        )
      } else {
        // New item — add with quantity 1
        updated = [...prev, { ...product, quantity: 1 }]
      }

      localStorage.setItem('cart', JSON.stringify(updated))
      return updated
    })

    setIsCartOpen(true)  // Open cart when item added
  }

  function updateQuantity(productId, newQuantity) {
    setCart(prev => {
      let updated
      if (newQuantity <= 0) {
        updated = prev.filter(item => item.id !== productId)
      } else {
        updated = prev.map(item =>
          item.id === productId
            ? { ...item, quantity: newQuantity }
            : item
        )
      }
      localStorage.setItem('cart', JSON.stringify(updated))
      return updated
    })
  }

  function removeFromCart(productId) {
    setCart(prev => {
      const updated = prev.filter(item => item.id !== productId)
      localStorage.setItem('cart', JSON.stringify(updated))
      return updated
    })
  }

  const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0)

  return (
    <div className="store-container">
      <header className="store-header">
        <h1>The Little Shop</h1>
        <button
          className="cart-toggle"
          onClick={() => setIsCartOpen(!isCartOpen)}
        >
          🛒 Cart ({cartCount})
        </button>
      </header>

      <div className="product-grid">
        {products.map(product => (
          <ProductCard
            key={product.id}
            product={product}
            onAddToCart={addToCart}
          />
        ))}
      </div>

      <Cart
        items={cart}
        isOpen={isCartOpen}
        onClose={() => setIsCartOpen(false)}
        onUpdateQuantity={updateQuantity}
        onRemove={removeFromCart}
      />
    </div>
  )
}

Step 3: Build the Shopping Cart

AI Prompt

"Build a slide-out shopping cart component for my Next.js store. Requirements: (1) Slides in from the right side of the screen, (2) Shows each cart item with: product name, unit price, quantity controls (– and + buttons), line total, and a remove button, (3) Shows cart subtotal at the bottom, (4) Has a 'Checkout' button that POSTs cart items to /api/checkout and redirects to the Stripe URL, (5) Has a close button (X) and clicking the backdrop also closes it, (6) Cart persists in localStorage, (7) Empty state shows 'Your cart is empty' with a link back to shopping. Dark theme, smooth slide animation."

The Cart Component

// components/Cart.js
import { useState } from 'react'

export default function Cart({ items, isOpen, onClose, onUpdateQuantity, onRemove }) {
  const [isLoading, setIsLoading] = useState(false)

  const subtotal = items.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  )

  async function handleCheckout() {
    setIsLoading(true)

    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          items: items.map(item => ({
            id: item.id,
            name: item.name,
            price: item.price,
            quantity: item.quantity,
            image: item.image
          }))
        })
      })

      const { url } = await response.json()

      // Redirect to Stripe's hosted checkout page
      window.location.href = url

    } catch (error) {
      console.error('Checkout error:', error)
      alert('Something went wrong. Please try again.')
      setIsLoading(false)
    }
  }

  return (
    <>
      {/* Backdrop */}
      {isOpen && (
        <div className="cart-backdrop" onClick={onClose} />
      )}

      {/* Cart panel */}
      <div className={`cart-panel ${isOpen ? 'open' : ''}`}>
        <div className="cart-header">
          <h2>Your Cart</h2>
          <button className="cart-close" onClick={onClose}>✕</button>
        </div>

        {items.length === 0 ? (
          <div className="cart-empty">
            <p>Your cart is empty</p>
            <button onClick={onClose}>Continue Shopping</button>
          </div>
        ) : (
          <>
            <div className="cart-items">
              {items.map(item => (
                <div key={item.id} className="cart-item">
                  <div className="cart-item-info">
                    <h4>{item.name}</h4>
                    <p className="cart-item-price">
                      ${(item.price / 100).toFixed(2)} each
                    </p>
                  </div>
                  <div className="cart-item-controls">
                    <button onClick={() =>
                      onUpdateQuantity(item.id, item.quantity - 1)
                    }>–</button>
                    <span>{item.quantity}</span>
                    <button onClick={() =>
                      onUpdateQuantity(item.id, item.quantity + 1)
                    }>+</button>
                  </div>
                  <div className="cart-item-total">
                    ${(item.price * item.quantity / 100).toFixed(2)}
                  </div>
                  <button
                    className="cart-item-remove"
                    onClick={() => onRemove(item.id)}
                  >✕</button>
                </div>
              ))}
            </div>

            <div className="cart-footer">
              <div className="cart-subtotal">
                <span>Subtotal</span>
                <span>${(subtotal / 100).toFixed(2)}</span>
              </div>
              <button
                className="checkout-btn"
                onClick={handleCheckout}
                disabled={isLoading}
              >
                {isLoading ? 'Redirecting to Checkout...' : 'Checkout'}
              </button>
            </div>
          </>
        )}
      </div>
    </>
  )
}

Understanding Cart State with localStorage

The cart uses the same localStorage pattern you learned in the to-do app — but now it's storing products instead of tasks. Every time the cart changes (add, remove, update quantity), we save the entire cart array to localStorage. When the page loads, we read it back.

This means your customer can add items, close the browser, come back tomorrow, and their cart is still there. No accounts, no database, no backend needed for this part.

The one thing to watch: localStorage is per-device and per-browser. If someone adds items on their phone and then switches to their laptop, they'll see an empty cart. That's fine for an MVP. User accounts with server-side cart storage is a "later" feature.

Step 4: Connect Stripe Checkout

This is the part that sounds scary but is actually the simplest piece of the whole project. Here's why:

You are NOT building a payment form. You're not handling credit card numbers. You're not dealing with PCI compliance. You're not storing sensitive financial data. Stripe Checkout is a hosted payment page that Stripe runs on their servers. Your job is to tell Stripe what the customer is buying, get a URL back, and redirect the customer there.

AI Prompt

"Create a Next.js API route at pages/api/checkout.js that: (1) Receives a POST request with an items array (each item has id, name, price in cents, quantity, and image URL), (2) Creates a Stripe Checkout Session using the Stripe Node.js SDK, (3) Uses mode: 'payment' for one-time purchases, (4) Maps each item to a line_items entry with price_data (currency: 'usd', unit_amount from item price, product_data with name and image), (5) Sets success_url to /success?session_id={CHECKOUT_SESSION_ID}, (6) Sets cancel_url to /canceled, (7) Returns the session URL as JSON. Use the STRIPE_SECRET_KEY environment variable. Do NOT use Stripe Price IDs — use inline price_data so products come from our JSON file."

The Checkout API Route

// pages/api/checkout.js
import Stripe from 'stripe'

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

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { items } = req.body

    // Validate items exist and have required fields
    if (!items || !items.length) {
      return res.status(400).json({ error: 'No items provided' })
    }

    // Create line items for Stripe
    const line_items = items.map(item => ({
      price_data: {
        currency: 'usd',
        unit_amount: item.price,  // Already in cents
        product_data: {
          name: item.name,
          images: item.image ? [`${process.env.NEXT_PUBLIC_URL}${item.image}`] : []
        }
      },
      quantity: item.quantity
    }))

    // Create the Stripe Checkout Session
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items,
      success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`,
      shipping_address_collection: {
        allowed_countries: ['US', 'CA']  // Add your shipping countries
      }
    })

    // Return the URL — the frontend will redirect here
    res.status(200).json({ url: session.url })

  } catch (error) {
    console.error('Stripe error:', error)
    res.status(500).json({ error: 'Failed to create checkout session' })
  }
}

How Stripe Checkout Actually Works

Let's trace exactly what happens when a customer clicks "Checkout":

  1. Your frontend sends the cart items to /api/checkout
  2. Your API route calls Stripe's API to create a Checkout Session
  3. Stripe returns a URL like https://checkout.stripe.com/c/pay/cs_live_abc123...
  4. Your frontend redirects the customer to that URL
  5. The customer fills out payment info on Stripe's secure, hosted page (not your site)
  6. After payment, Stripe redirects back to your /success page

You never see the credit card number. You never store it. You never transmit it. Stripe handles all of that on their infrastructure. This is why Stripe Checkout is the right choice for any store that doesn't need a fully custom checkout experience.

Setting Up Stripe

Before this works, you need a Stripe account:

  1. Go to stripe.com and create a free account
  2. In the Stripe Dashboard, go to Developers → API Keys
  3. Copy your Secret Key (starts with sk_test_ for testing)
  4. Add it to your .env.local file:
# .env.local
STRIPE_SECRET_KEY=sk_test_your_key_here
NEXT_PUBLIC_URL=http://localhost:3000

Use test mode first. Stripe gives you test API keys that simulate payments without charging real money. Test card number: 4242 4242 4242 4242, any future expiry, any CVC. Only switch to live keys when you're ready to accept real payments.

Step 5: Build Success and Cancel Pages

AI Prompt

"Create two pages for my Next.js store: (1) pages/success.js — order confirmation page. Show 'Thank you for your order!' heading, a checkmark icon, order number (from the session_id query param), and a 'Continue Shopping' link. Clear the cart from localStorage when this page loads. (2) pages/canceled.js — payment canceled page. Show 'Payment canceled' heading, reassuring message that they weren't charged, their cart is still saved, and a 'Return to Cart' button. Both pages should match the dark theme."

The Success Page

// pages/success.js
import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function SuccessPage() {
  const router = useRouter()
  const { session_id } = router.query

  useEffect(() => {
    // Clear the cart — they've paid!
    localStorage.removeItem('cart')
  }, [])

  return (
    <div className="result-page">
      <div className="result-icon success">✓</div>
      <h1>Thank You for Your Order!</h1>
      <p>Your payment was successful. You'll receive a
         confirmation email shortly.</p>
      {session_id && (
        <p className="order-ref">
          Order reference: {session_id.slice(-8).toUpperCase()}
        </p>
      )}
      <a href="/" className="continue-shopping">Continue Shopping</a>
    </div>
  )
}

The useEffect that clears localStorage is important — once they've paid, you don't want them refreshing the page and seeing a full cart again. The session_id in the URL comes from Stripe and lets you look up the order later if needed.

Step 6: Style Your Store

AI Prompt

"Write the CSS for my e-commerce store. Requirements: (1) Dark theme, background #0A0E1A, card backgrounds #12162B, (2) Product grid: CSS Grid, 3 columns desktop, 2 tablet, 1 mobile, 20px gap, (3) Product cards: rounded corners, subtle border, hover lift effect with box-shadow, (4) Add to Cart button: accent color #6C63FF, full width of card, (5) Cart panel: fixed right side, 380px wide, slides in with transform, backdrop blur overlay, (6) Checkout button: full width, green (#22C55E), prominent, (7) Quantity controls: inline flex, compact, (8) All transitions smooth (0.2s ease), (9) Mobile: cart takes full width, product grid single column, (10) Make it look like a real store, not a tutorial project."

We won't dump the full CSS here — it's mostly visual polish. The important layout pieces are the CSS Grid for products and the fixed-position slide-out for the cart. Tell your AI exactly what you want, and iterate. The prompts above give it everything it needs.

Step 7: Deploy Your Store

Your store needs a server (for the API route that talks to Stripe). Here are your deployment options, ranked by simplicity:

Option 1: Vercel (Easiest)

Vercel built Next.js, so the integration is seamless:

# Push to GitHub, then connect to Vercel
git add .
git commit -m "E-commerce store — ready for launch"
git push origin main

# In Vercel dashboard:
# 1. Import your GitHub repo
# 2. Add environment variables:
#    STRIPE_SECRET_KEY = sk_live_your_real_key
#    NEXT_PUBLIC_URL = https://your-store.vercel.app
# 3. Deploy

Your API routes automatically become serverless functions. Zero configuration.

Option 2: Netlify

Works with the @netlify/plugin-nextjs plugin. Same process: push to GitHub, connect repo, add environment variables. Netlify converts your API routes to Netlify Functions automatically.

Option 3: VPS (Full Control)

If you have a VPS (DigitalOcean, Hetzner, your own server), you can run Next.js directly with npm run build && npm start behind nginx. More setup, but you get full control over the server, custom domain, and no vendor lock-in.

Environment Variables in Production

Critical: When you go live, switch from sk_test_ to sk_live_ in your Stripe secret key. Everything else stays the same. Test thoroughly in test mode first — once you switch to live keys, real money moves.

Going Live Checklist

Before you share your store URL with the world, run through this:

  • Test the full flow: Add items → checkout → pay with test card → success page → cart cleared
  • Mobile responsive: Test on your phone. Product grid, cart, and checkout all work?
  • Product images: All loading? Right aspect ratios? WebP format for speed?
  • Prices correct: Double-check every price in products.json (remember: cents!)
  • Environment variables: STRIPE_SECRET_KEY and NEXT_PUBLIC_URL set in production
  • Stripe live mode: Switch from test to live API keys
  • Stripe Dashboard: Check "Payments" tab after a test purchase to verify it appears
  • Cancel flow: Test what happens when customer clicks "Back" on Stripe's page
  • Error states: What happens if the API route fails? Does the user see a helpful message?
  • Shipping countries: Update allowed_countries in the checkout API if you ship internationally

What AI Gets Wrong About E-Commerce

AI tools are incredible for building stores fast, but they have consistent blind spots. Watch for these:

1. Building a Custom Payment Form

This is the #1 mistake. Your AI will enthusiastically generate a beautiful payment form with credit card fields, expiry date inputs, and a "Pay Now" button. Do not use this. Handling raw credit card data means PCI compliance, security audits, and enormous liability. Stripe Checkout exists specifically so you never touch payment data. Always redirect to Stripe's hosted page.

2. Over-Engineering the Cart

AI loves to build cart systems with Redux, context providers, custom hooks, cart middleware, and server-side cart storage. For a store with 5-20 products and moderate traffic? localStorage and React state are all you need. You can refactor to something more sophisticated when you have the traffic to justify it.

3. Forgetting Mobile

AI generates desktop layouts first. If you don't explicitly say "mobile responsive" in every prompt, you'll get a product grid that looks beautiful on a monitor and broken on an iPhone. More than 60% of online shopping happens on mobile. Test on your phone before you test on your laptop.

4. Skipping Error Handling

The happy path works great in AI-generated code. But what happens when: Stripe's API is down? The customer's card is declined? The network request fails? Your checkout API route should always have try/catch, and your frontend should show helpful error messages — not just a blank screen or console error.

5. Hardcoding Prices on the Frontend

AI might put prices directly in the JSX or calculate totals only on the frontend. The problem: a clever person could edit the JavaScript in their browser and change the price. Always validate prices server-side in your checkout API route. Read from your products.json (or database) on the server, not from whatever the frontend sends.

Bonus: Server-Side Price Validation

The checkout API route above trusts the frontend-provided prices. Here's the hardened version that validates against your source of truth:

// pages/api/checkout.js (hardened version)
import Stripe from 'stripe'
import products from '../../data/products.json'

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

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const { items } = req.body

    // Validate each item against our product catalog
    const line_items = items.map(item => {
      // Look up the REAL product from our data
      const product = products.find(p => p.id === item.id)

      if (!product) {
        throw new Error(`Product not found: ${item.id}`)
      }

      return {
        price_data: {
          currency: 'usd',
          unit_amount: product.price,  // Server-side price — can't be tampered
          product_data: {
            name: product.name,
            images: product.image
              ? [`${process.env.NEXT_PUBLIC_URL}${product.image}`]
              : []
          }
        },
        quantity: Math.min(Math.max(1, item.quantity), 99)  // Clamp quantity
      }
    })

    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      line_items,
      success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`
    })

    res.status(200).json({ url: session.url })

  } catch (error) {
    console.error('Checkout error:', error)
    res.status(500).json({ error: error.message })
  }
}

The key difference: product.price comes from your JSON file on the server, not from item.price that the browser sent. The frontend can send whatever it wants — you always charge the real price.

What to Add Next

Your store works. Customers can browse, add to cart, and pay. Here's the roadmap for leveling up, in order of impact:

1. Email Confirmations

Use Stripe Webhooks to trigger an email when a payment succeeds. Services like Resend or SendGrid make this straightforward. Your AI can generate the webhook handler — it's one more API route.

2. Product Categories & Filtering

Add a category field to each product (already in our JSON) and build filter buttons. Same pattern as the to-do app's All/Active/Completed filters — just filtering products instead of tasks.

3. User Accounts

When you need order history, saved carts across devices, or customer profiles, add authentication. NextAuth.js or Clerk makes this painless with Google/email sign-in. This is when you also move from localStorage to a database for cart storage.

4. Inventory Tracking

Move products from JSON to a database (Supabase is the easiest step). Add a stock field. Decrement on successful payment (via Stripe webhook). Show "Out of Stock" when it hits zero.

5. Discount Codes

Stripe supports promotion codes natively in Checkout Sessions. Add allow_promotion_codes: true to your session creation, and manage codes in the Stripe Dashboard. Zero code changes on your end.

Frequently Asked Questions

Do I need a backend to use Stripe Checkout?

Yes, but it's minimal. You need a single server-side function to create a Stripe Checkout Session — about 20 lines of code. This can be a Next.js API route, a Vercel serverless function, or a Netlify function. You never handle credit card data directly. Stripe's hosted checkout page handles all payment processing on their secure infrastructure.

How much does Stripe cost?

Stripe charges 2.9% + 30¢ per successful card charge in the US (as of 2026). No monthly fees, no setup fees, no minimums. You only pay when you make a sale. For a $25 product, Stripe takes about $1.03. You can absorb this or build it into your pricing.

Can I sell digital products with this setup?

Absolutely — digital products are actually easier because there's no shipping. After successful payment, redirect to a success page with a download link, or use Stripe Webhooks to send a fulfillment email with the download. Remove the shipping_address_collection from your checkout session for digital-only products.

Should I use a JSON file or a database for products?

Start with JSON. If you have fewer than 50 products that don't change often, JSON is simpler, faster, and needs zero infrastructure. It's version controlled with git, loads instantly, and AI tools can easily generate and edit it. Move to a database when you need real-time inventory, non-technical product editing, or hundreds of items.

What about inventory management?

For an MVP, manage stock manually and update your JSON file. When you're ready to scale, move to a database and use Stripe Webhooks to auto-decrement stock on each successful payment. Most small stores with under 50 products don't need automated inventory on day one.

What to Learn Next