Skip to content
All essays
WebJanuary 15, 202617 min

Building a Modern E-commerce Platform from Scratch

Learn how to architect and build a scalable e-commerce platform with modern technologies, covering product management, cart functionality, checkout process, and payment integration

Ü
Ümit Uz
Mobile & Full Stack Developer

E-commerce has transformed from a novelty to a necessity. Building your own e-commerce platform gives you complete control over the customer experience, integration capabilities, and business logic. This comprehensive guide will walk you through creating a production-ready e-commerce platform using modern technologies.

Architecture Overview

A modern e-commerce platform consists of several core systems:

  • Product Catalog: Manage products, variants, inventory, and categories
  • Shopping Cart: Persistent cart functionality for users
  • Checkout Flow: Seamless multi-step checkout process
  • Payment Processing: Secure payment integration
  • Order Management: Order lifecycle management
  • User Accounts: Customer authentication and profiles
  • Admin Dashboard: Store management and analytics

Database Design

Let's start with a robust database schema that can handle the complexity of e-commerce.

Core Tables

sql
-- Products table
CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  slug VARCHAR(255) UNIQUE NOT NULL,
  description TEXT,
  price DECIMAL(10, 2) NOT NULL,
  compare_at_price DECIMAL(10, 2),
  sku VARCHAR(100) UNIQUE,
  barcode VARCHAR(100),
  track_inventory BOOLEAN DEFAULT true,
  category_id UUID REFERENCES categories(id),
  status VARCHAR(50) DEFAULT 'active', -- active, draft, archived
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Product variants (size, color, etc.)
CREATE TABLE product_variants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  product_id UUID REFERENCES products(id) ON DELETE CASCADE,
  name VARCHAR(100) NOT NULL,
  price DECIMAL(10, 2),
  sku VARCHAR(100) UNIQUE,
  inventory_quantity INT DEFAULT 0,
  weight DECIMAL(10, 2),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Variant attributes (color: red, size: large)
CREATE TABLE variant_attributes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  variant_id UUID REFERENCES product_variants(id) ON DELETE CASCADE,
  attribute_name VARCHAR(50) NOT NULL,
  attribute_value VARCHAR(100) NOT NULL
);

-- Categories
CREATE TABLE categories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(100) NOT NULL,
  slug VARCHAR(100) UNIQUE NOT NULL,
  description TEXT,
  parent_id UUID REFERENCES categories(id),
  image_url VARCHAR(500),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Product images
CREATE TABLE product_images (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  product_id UUID REFERENCES products(id) ON DELETE CASCADE,
  image_url VARCHAR(500) NOT NULL,
  alt_text VARCHAR(255),
  position INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Shopping cart
CREATE TABLE cart_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  session_id VARCHAR(255), -- For guest carts
  product_id UUID REFERENCES products(id) ON DELETE CASCADE,
  variant_id UUID REFERENCES product_variants(id) ON DELETE SET NULL,
  quantity INT NOT NULL CHECK (quantity > 0),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Orders
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_number VARCHAR(50) UNIQUE NOT NULL,
  user_id UUID REFERENCES users(id),
  email VARCHAR(255) NOT NULL,
  status VARCHAR(50) DEFAULT 'pending', -- pending, processing, shipped, delivered, cancelled
  subtotal DECIMAL(10, 2) NOT NULL,
  tax DECIMAL(10, 2) DEFAULT 0,
  shipping DECIMAL(10, 2) DEFAULT 0,
  total DECIMAL(10, 2) NOT NULL,
  currency VARCHAR(3) DEFAULT 'USD',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Order items
CREATE TABLE order_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
  product_id UUID REFERENCES products(id),
  variant_id UUID REFERENCES product_variants(id),
  product_name VARCHAR(255) NOT NULL,
  variant_name VARCHAR(100),
  quantity INT NOT NULL,
  price DECIMAL(10, 2) NOT NULL,
  total DECIMAL(10, 2) NOT NULL
);

-- Addresses
CREATE TABLE addresses (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  first_name VARCHAR(100) NOT NULL,
  last_name VARCHAR(100) NOT NULL,
  address_line1 VARCHAR(255) NOT NULL,
  address_line2 VARCHAR(255),
  city VARCHAR(100) NOT NULL,
  state VARCHAR(100),
  postal_code VARCHAR(20) NOT NULL,
  country VARCHAR(2) NOT NULL,
  phone VARCHAR(50),
  is_default BOOLEAN DEFAULT false
);

Backend API Implementation

Let's build the backend API using Node.js, Express, and TypeScript.

Product Management API

typescript
import express from 'express';
import { Pool } from 'pg';
import { v4 as uuidv4 } from 'uuid';

const router = express.Router();
const pool = new Pool({ /* database config */ });

// List products with filtering and pagination
router.get('/products', async (req, res) => {
  try {
    const {
      page = 1,
      limit = 20,
      category,
      search,
      min_price,
      max_price,
      sort = 'created_at'
    } = req.query;

    const offset = (parseInt(page as string) - 1) * parseInt(limit as string);

    let query = `
      SELECT
        p.*,
        c.name as category_name,
        json_agg(DISTINCT pi.image_url) as images
      FROM products p
      LEFT JOIN categories c ON p.category_id = c.id
      LEFT JOIN product_images pi ON p.id = pi.product_id
      WHERE p.status = 'active'
    `;
    const params: any[] = [];

    // Category filter
    if (category) {
      params.push(category);
      query += ` AND c.slug = $${params.length}`;
    }

    // Search filter
    if (search) {
      params.push(`%${search}%`);
      query += ` AND (p.name ILIKE $${params.length} OR p.description ILIKE $${params.length})`;
    }

    // Price range filter
    if (min_price) {
      params.push(min_price);
      query += ` AND p.price >= $${params.length}`;
    }
    if (max_price) {
      params.push(max_price);
      query += ` AND p.price <= $${params.length}`;
    }

    query += ` GROUP BY p.id, c.name`;

    // Sorting
    const allowedSorts = ['created_at', 'price', 'name'];
    const sortBy = allowedSorts.includes(sort as string) ? sort : 'created_at';
    query += ` ORDER BY p.${sortBy} DESC`;

    // Pagination
    params.push(parseInt(limit as string), offset);
    query += ` LIMIT $${params.length - 1} OFFSET $${params.length}`;

    const result = await pool.query(query, params);

    // Get total count for pagination
    let countQuery = `SELECT COUNT(DISTINCT p.id) FROM products p WHERE p.status = 'active'`;
    const countParams: any[] = [];

    if (category) {
      countParams.push(category);
      countQuery += ` AND p.category_id = (SELECT id FROM categories WHERE slug = $${countParams.length})`;
    }

    const countResult = await pool.query(countQuery, countParams);
    const total = parseInt(countResult.rows[0].count);

    res.json({
      data: result.rows,
      pagination: {
        page: parseInt(page as string),
        limit: parseInt(limit as string),
        total,
        totalPages: Math.ceil(total / parseInt(limit as string))
      }
    });
  } catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get single product with variants
router.get('/products/:slug', async (req, res) => {
  try {
    const { slug } = req.params;

    const query = `
      SELECT
        p.*,
        c.name as category_name,
        json_agg(
          json_build_object(
            'id', v.id,
            'name', v.name,
            'price', v.price,
            'sku', v.sku,
            'inventory_quantity', v.inventory_quantity,
            'attributes', (
              SELECT json_agg(json_build_object('name', attribute_name, 'value', attribute_value))
              FROM variant_attributes
              WHERE variant_id = v.id
            )
          )
        ) as variants,
        json_agg(DISTINCT json_build_object('url', pi.image_url, 'alt', pi.alt_text)) as images
      FROM products p
      LEFT JOIN categories c ON p.category_id = c.id
      LEFT JOIN product_variants v ON p.id = v.product_id
      LEFT JOIN product_images pi ON p.id = pi.product_id
      WHERE p.slug = $1 AND p.status = 'active'
      GROUP BY p.id, c.name
    `;

    const result = await pool.query(query, [slug]);

    if (result.rows.length === 0) {
      return res.status(404).json({ error: 'Product not found' });
    }

    res.json(result.rows[0]);
  } catch (error) {
    console.error('Error fetching product:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

export default router;

Shopping Cart System

Cart API Implementation

typescript
// Add item to cart
router.post('/cart/items', async (req, res) => {
  try {
    const { user_id, session_id, product_id, variant_id, quantity } = req.body;

    // Validate product exists and has sufficient inventory
    const productQuery = `
      SELECT p.id, p.name, p.track_inventory,
        COALESCE(v.inventory_quantity, p.inventory_quantity, 0) as available
      FROM products p
      LEFT JOIN product_variants v ON v.id = $1
      WHERE p.id = $2
    `;
    const productResult = await pool.query(productQuery, [variant_id, product_id]);

    if (productResult.rows.length === 0) {
      return res.status(404).json({ error: 'Product not found' });
    }

    const product = productResult.rows[0];

    if (product.track_inventory && product.available < quantity) {
      return res.status(400).json({ error: 'Insufficient inventory' });
    }

    // Check if item already exists in cart
    const existingQuery = `
      SELECT id, quantity FROM cart_items
      WHERE (user_id = $1 OR session_id = $2)
        AND product_id = $3
        AND (variant_id = $4 OR variant_id IS NULL)
    `;
    const existingResult = await pool.query(existingQuery, [
      user_id,
      session_id,
      product_id,
      variant_id
    ]);

    if (existingResult.rows.length > 0) {
      // Update quantity
      const updateQuery = `
        UPDATE cart_items
        SET quantity = quantity + $1, updated_at = CURRENT_TIMESTAMP
        WHERE id = $2
        RETURNING *
      `;
      const updateResult = await pool.query(updateQuery, [
        quantity,
        existingResult.rows[0].id
      ]);
      return res.json(updateResult.rows[0]);
    }

    // Add new item
    const insertQuery = `
      INSERT INTO cart_items (user_id, session_id, product_id, variant_id, quantity)
      VALUES ($1, $2, $3, $4, $5)
      RETURNING *
    `;
    const insertResult = await pool.query(insertQuery, [
      user_id,
      session_id,
      product_id,
      variant_id,
      quantity
    ]);

    res.status(201).json(insertResult.rows[0]);
  } catch (error) {
    console.error('Error adding to cart:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Get cart contents
router.get('/cart', async (req, res) => {
  try {
    const { user_id, session_id } = req.query;

    const query = `
      SELECT
        ci.*,
        p.name as product_name,
        p.price as product_price,
        p.slug as product_slug,
        json_agg(pi.image_url) as product_images,
        v.name as variant_name,
        v.price as variant_price
      FROM cart_items ci
      JOIN products p ON ci.product_id = p.id
      LEFT JOIN product_images pi ON p.id = pi.product_id
      LEFT JOIN product_variants v ON ci.variant_id = v.id
      WHERE ci.user_id = $1 OR ci.session_id = $2
      GROUP BY ci.id, p.name, p.price, p.slug, v.name, v.price
      ORDER BY ci.created_at DESC
    `;

    const result = await pool.query(query, [user_id, session_id]);

    // Calculate totals
    const subtotal = result.rows.reduce((sum, item) => {
      const price = item.variant_price || item.product_price;
      return sum + (price * item.quantity);
    }, 0);

    res.json({
      items: result.rows,
      totals: {
        subtotal,
        item_count: result.rows.reduce((sum, item) => sum + item.quantity, 0)
      }
    });
  } catch (error) {
    console.error('Error fetching cart:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

Checkout Process

Creating an Order

typescript
router.post('/checkout', async (req, res) => {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');

    const {
      user_id,
      email,
      shipping_address,
      billing_address,
      payment_method_id,
      cart_items
    } = req.body;

    // Calculate order totals
    let subtotal = 0;
    for (const item of cart_items) {
      const price = item.variant_price || item.product_price;
      subtotal += price * item.quantity;
    }

    const tax = subtotal * 0.08; // 8% tax rate
    const shipping = subtotal > 100 ? 0 : 9.99; // Free shipping over $100
    const total = subtotal + tax + shipping;

    // Generate order number
    const order_number = `ORD-${Date.now()}-${Math.random().toString(36).substr(2, 9).toUpperCase()}`;

    // Create order
    const orderQuery = `
      INSERT INTO orders (
        order_number, user_id, email, status,
        subtotal, tax, shipping, total
      )
      VALUES ($1, $2, $3, 'pending', $4, $5, $6, $7)
      RETURNING *
    `;
    const orderResult = await client.query(orderQuery, [
      order_number,
      user_id,
      email,
      subtotal,
      tax,
      shipping,
      total
    ]);

    const order = orderResult.rows[0];

    // Create order items
    for (const item of cart_items) {
      const price = item.variant_price || item.product_price;
      const itemTotal = price * item.quantity;

      await client.query(
        `INSERT INTO order_items (
          order_id, product_id, variant_id,
          product_name, variant_name, quantity, price, total
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
        [
          order.id,
          item.product_id,
          item.variant_id,
          item.product_name,
          item.variant_name,
          item.quantity,
          price,
          itemTotal
        ]
      );

      // Update inventory
      if (item.track_inventory) {
        await client.query(
          `UPDATE product_variants
           SET inventory_quantity = inventory_quantity - $1
           WHERE id = $2`,
          [item.quantity, item.variant_id]
        );
      }
    }

    // Process payment
    const paymentResult = await processPayment(
      payment_method_id,
      total,
      order_number
    );

    if (!paymentResult.success) {
      await client.query('ROLLBACK');
      return res.status(400).json({ error: 'Payment failed' });
    }

    // Clear cart
    await client.query(
      `DELETE FROM cart_items WHERE user_id = $1`,
      [user_id]
    );

    await client.query('COMMIT');

    res.status(201).json({
      order,
      payment: paymentResult
    });
  } catch (error) {
    await client.query('ROLLBACK');
    console.error('Checkout error:', error);
    res.status(500).json({ error: 'Checkout failed' });
  } finally {
    client.release();
  }
});

Frontend Implementation

Product Listing Component

typescript
import React, { useState, useEffect } from 'react';
import { ProductCard } from './ProductCard';
import { FilterSidebar } from './FilterSidebar';

interface Product {
  id: string;
  name: string;
  slug: string;
  price: number;
  images: string[];
  category_name: string;
}

export const ProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [filters, setFilters] = useState({
    category: '',
    min_price: '',
    max_price: '',
    search: ''
  });

  useEffect(() => {
    fetchProducts();
  }, [filters]);

  const fetchProducts = async () => {
    setLoading(true);
    try {
      const params = new URLSearchParams();
      Object.entries(filters).forEach(([key, value]) => {
        if (value) params.append(key, value);
      });

      const response = await fetch(`/api/products?${params}`);
      const data = await response.json();
      setProducts(data.data);
    } catch (error) {
      console.error('Error fetching products:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="flex gap-8">
        <FilterSidebar
          filters={filters}
          onChange={setFilters}
        />
        <div className="flex-1">
          {loading ? (
            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
              {[...Array(6)].map((_, i) => (
                <div key={i} className="animate-pulse bg-gray-200 h-96 rounded-lg" />
              ))}
            </div>
          ) : (
            <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
              {products.map((product) => (
                <ProductCard key={product.id} product={product} />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

Security Considerations

E-commerce platforms handle sensitive data and require robust security:

PCI Compliance

If you handle credit card data directly, you must be PCI compliant. However, using payment processors like Stripe or PayPal shifts most of this burden to them.

typescript
// Never store raw credit card numbers
// Use payment processor tokens instead

interface PaymentMethod {
  id: string;
  last_four: string; // Only store last 4 digits
  brand: string; // visa, mastercard, etc.
  token: string; // Payment processor token
}

Secure Checkout

  • Always use HTTPS
  • Validate all input data
  • Implement rate limiting
  • Use CSRF tokens
  • Secure session management
  • Regular security audits

Performance Optimization

Caching Strategy

typescript
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export async function getProduct(slug: string) {
  const cacheKey = `product:${slug}`;

  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Fetch from database
  const product = await fetchProductFromDB(slug);

  // Cache for 1 hour
  await redis.setex(cacheKey, 3600, JSON.stringify(product));

  return product;
}

Image Optimization

  • Use WebP format for modern browsers
  • Implement lazy loading
  • Serve responsive images
  • Use CDN for delivery

Conclusion

Building an e-commerce platform is a complex undertaking, but breaking it down into manageable components makes it achievable. Focus on:

  1. 1Solid database design - The foundation of your system
  2. 2Clean API architecture - Enables easy frontend integration
  3. 3Security first - Protect customer data at all costs
  4. 4Performance optimization - Fast sites convert better
  5. 5Scalability - Design for growth from day one

Start with the core features covered in this guide, then expand based on your specific needs. Remember, the best e-commerce platform is one that serves your customers' needs while supporting your business goals.

Enjoyed this essay? Share it.

Related essays

Next essay
Complete Stripe Integration Guide for E-commerce