← All projects

Node.js · Vercel Functions · E-Commerce

Luma Store

A full e-commerce storefront — product catalog, cart, and a real checkout flow where pricing is never trusted from the browser.

Product search and filtering, product detail pages, a cart, and a checkout flow that creates a real order. The important part: the client only ever sends product ids and quantities — every price, the subtotal, shipping, and tax are recomputed server-side from the product catalog in api/orders.js, so there's nothing to tamper with in the request. This is the same pattern a real online store needs: a client that proposes, and a backend that decides.

Live demo

embed
const CATALOG = require('./_catalog');

// In-memory order store — resets on cold start, same tradeoff used across
// this repo's demos. Prices are always recomputed server-side from the
// catalog below; the client only ever sends product ids and quantities,
// never prices, so there's nothing to tamper with in the request.
const orders = globalThis.__orders || (globalThis.__orders = new Map());

function nextOrderNumber() {
  const n = (orders.size + 1).toString().padStart(5, '0');
  return `LM-${n}`;
}

module.exports = (req, res) => {
  if (req.method === 'POST') {
    const items = Array.isArray(req.body && req.body.items) ? req.body.items : [];
    const customer = (req.body && req.body.customer) || {};

    if (!items.length) {
      res.status(400).json({ error: 'cart is empty' });
      return;
    }
    if (!customer.name || !customer.email || !customer.address) {
      res.status(400).json({ error: 'name, email and address are required' });
      return;
    }

    const lineItems = [];
    for (const item of items) {
      const product = CATALOG.find((p) => p.id === item.id);
      if (!product) {
        res.status(400).json({ error: `unknown product: ${item.id}` });
        return;
      }
      const qty = Math.max(1, Math.min(20, parseInt(item.qty, 10) || 1));
      lineItems.push({
        id: product.id,
        name: product.name,
        price: product.price,
        qty,
        lineTotal: Math.round(product.price * qty * 100) / 100,
      });
    }

    const subtotal = Math.round(lineItems.reduce((s, i) => s + i.lineTotal, 0) * 100) / 100;
    const shipping = subtotal >= 100 ? 0 : 8;
    const tax = Math.round(subtotal * 0.08 * 100) / 100;
    const total = Math.round((subtotal + shipping + tax) * 100) / 100;

    const order = {
      id: nextOrderNumber(),
      customer: { name: customer.name, email: customer.email, address: customer.address },
      items: lineItems,
      subtotal,
      shipping,
      tax,
      total,
      createdAt: new Date().toISOString(),
    };
    orders.set(order.id, order);

    res.status(201).json(order);
    return;
  }

  if (req.method === 'GET') {
    const id = (req.query.id || '').toString();
    const order = orders.get(id);
    if (!order) {
      res.status(404).json({ error: 'order not found' });
      return;
    }
    res.status(200).json(order);
    return;
  }

  res.status(405).json({ error: 'method not allowed' });
};
const checkoutEl = document.getElementById('checkout');

function previewTotals(items) {
  const subtotal = items.reduce((s, i) => s + i.product.price * i.qty, 0);
  const shipping = subtotal >= 100 || subtotal === 0 ? 0 : 8;
  const tax = subtotal * 0.08;
  const total = subtotal + shipping + tax;
  return { subtotal, shipping, tax, total };
}

async function load() {
  const cart = getCart();
  if (!cart.length) {
    checkoutEl.innerHTML = `<p class="empty-state">Your cart is empty. <a href="index.html">Go shopping →</a></p>`;
    return;
  }

  const items = await Promise.all(cart.map(async (entry) => {
    const res = await fetch(`/api/products?id=${encodeURIComponent(entry.id)}`);
    const product = await res.json();
    return { ...entry, product };
  }));
  const t = previewTotals(items);

  checkoutEl.innerHTML = `
    <form id="checkout-form">
      <div class="form-row">
        <div class="form-group"><label>Full name</label><input name="name" required></div>
        <div class="form-group"><label>Email</label><input type="email" name="email" required></div>
      </div>
      <div class="form-group"><label>Shipping address</label><textarea name="address" rows="3" required></textarea></div>
      <div class="form-row">
        <div class="form-group"><label>Card number</label><input value="4242 4242 4242 4242" disabled></div>
        <div class="form-group"><label>Expiry / CVC</label><input value="12/29 · 123" disabled></div>
      </div>
      <p style="font-size:12px;color:var(--muted);margin:-6px 0 18px">This is a demo — payment fields are disabled, no real charge happens. The order itself is a real API call.</p>
      <button type="submit" class="btn primary block">Place order — ${money(t.total)}</button>
      <div class="error-text" id="error"></div>
    </form>
    <div class="summary-card">
      <h3>Order summary</h3>
      ${items.map((i) => `
        <div class="summary-line"><span>${i.product.name} × ${i.qty}</span><span>${money(i.product.price * i.qty)}</span></div>
      `).join('')}
      <div class="summary-line"><span>Shipping</span><span>${t.shipping === 0 ? 'Free' : money(t.shipping)}</span></div>
      <div class="summary-line"><span>Estimated tax</span><span>${money(t.tax)}</span></div>
      <div class="summary-line total"><span>Total</span><span>${money(t.total)}</span></div>
    </div>
  `;

  document.getElementById('checkout-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const form = e.target;
    const btn = form.querySelector('button[type=submit]');
    const errorEl = document.getElementById('error');
    errorEl.textContent = '';
    btn.disabled = true;
    btn.textContent = 'Placing order…';

    const payload = {
      items: cart.map((c) => ({ id: c.id, qty: c.qty })),
      customer: {
        name: form.name.value,
        email: form.email.value,
        address: form.address.value,
      },
    };

    const res = await fetch('/api/orders', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    const data = await res.json();

    if (!res.ok) {
      errorEl.textContent = data.error || 'Something went wrong.';
      btn.disabled = false;
      btn.textContent = `Place order — ${money(t.total)}`;
      return;
    }

    localStorage.removeItem('luma_cart_v1');
    updateCartBadge();
    renderConfirmation(data);
  });
}

function renderConfirmation(order) {
  document.getElementById('checkout-wrap').innerHTML = `
    <div class="confirmation">
      <div class="check">✓</div>
      <h1>Order confirmed</h1>
      <p class="order-no">Order <strong>${order.id}</strong> — confirmation sent to ${order.customer.email}</p>
      <div class="order-summary">
        ${order.items.map((i) => `
          <div class="summary-line"><span>${i.name} × ${i.qty}</span><span>${money(i.lineTotal)}</span></div>
        `).join('')}
        <div class="summary-line"><span>Shipping</span><span>${order.shipping === 0 ? 'Free' : money(order.shipping)}</span></div>
        <div class="summary-line"><span>Tax</span><span>${money(order.tax)}</span></div>
        <div class="summary-line total"><span>Total</span><span>${money(order.total)}</span></div>
      </div>
      <a href="index.html" class="btn primary" style="margin-top:28px">Continue shopping</a>
    </div>
  `;
}

load();