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
-- 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
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
// 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
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
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.
// 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
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:
- 1Solid database design - The foundation of your system
- 2Clean API architecture - Enables easy frontend integration
- 3Security first - Protect customer data at all costs
- 4Performance optimization - Fast sites convert better
- 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.