E-commerce Application

Learn how to build a complete e-commerce application with Query-2jz, including products, orders, payments, and inventory management.

Project Overview

Build a full-featured e-commerce application with product catalog, shopping cart, order management, and payment processing.

Features

  • • Product catalog with categories and search
  • • Shopping cart and wishlist
  • • User authentication and profiles
  • • Order management and tracking
  • • Payment processing integration
  • • Inventory management
  • • Admin dashboard
  • • Real-time notifications

Project Structure

e-commerce-app/
├── src/
│   ├── models/
│   │   ├── User.ts
│   │   ├── Product.ts
│   │   ├── Category.ts
│   │   ├── Order.ts
│   │   ├── Cart.ts
│   │   └── Payment.ts
│   ├── services/
│   │   ├── ProductService.ts
│   │   ├── OrderService.ts
│   │   ├── PaymentService.ts
│   │   └── NotificationService.ts
│   ├── controllers/
│   │   ├── ProductController.ts
│   │   ├── OrderController.ts
│   │   └── UserController.ts
│   ├── config/
│   │   └── query-2jz.config.ts
│   └── index.ts
├── package.json
├── tsconfig.json
└── README.md

Data Models

Define comprehensive data models for the e-commerce application.

Product Model

// src/models/Product.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  comparePrice?: number;
  sku: string;
  barcode?: string;
  categoryId: string;
  brand?: string;
  weight?: number;
  dimensions?: {
    length: number;
    width: number;
    height: number;
  };
  images: string[];
  tags: string[];
  isActive: boolean;
  isDigital: boolean;
  inventory: {
    quantity: number;
    reserved: number;
    available: number;
  };
  seo: {
    title?: string;
    description?: string;
    keywords?: string[];
  };
  createdAt: Date;
  updatedAt: Date;
  category?: Category;
  variants?: ProductVariant[];
  reviews?: Review[];
}

export interface ProductVariant {
  id: string;
  productId: string;
  name: string;
  sku: string;
  price: number;
  comparePrice?: number;
  inventory: {
    quantity: number;
    reserved: number;
    available: number;
  };
  attributes: Record<string, string>;
  isActive: boolean;
  createdAt: Date;
  updatedAt: Date;
  product?: Product;
}

export interface Review {
  id: string;
  productId: string;
  userId: string;
  rating: number;
  title: string;
  content: string;
  isVerified: boolean;
  helpful: number;
  createdAt: Date;
  updatedAt: Date;
  product?: Product;
  user?: User;
}

Order Model

// src/models/Order.ts
export interface Order {
  id: string;
  orderNumber: string;
  userId: string;
  status: 'pending' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
  items: OrderItem[];
  shipping: {
    address: Address;
    method: string;
    cost: number;
    trackingNumber?: string;
    estimatedDelivery?: Date;
  };
  billing: {
    address: Address;
    method: string;
  };
  payment: {
    method: string;
    status: 'pending' | 'paid' | 'failed' | 'refunded';
    transactionId?: string;
    amount: number;
    currency: string;
  };
  totals: {
    subtotal: number;
    tax: number;
    shipping: number;
    discount: number;
    total: number;
  };
  notes?: string;
  createdAt: Date;
  updatedAt: Date;
  user?: User;
  items?: OrderItem[];
}

export interface OrderItem {
  id: string;
  orderId: string;
  productId: string;
  variantId?: string;
  quantity: number;
  price: number;
  total: number;
  createdAt: Date;
  updatedAt: Date;
  order?: Order;
  product?: Product;
  variant?: ProductVariant;
}

export interface Address {
  firstName: string;
  lastName: string;
  company?: string;
  address1: string;
  address2?: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
  phone?: string;
}

Query-2jz Configuration

Configure Query-2jz with all the necessary models and relationships.

// src/config/query-2jz.config.ts
import { Query-2jzConfig } from 'query-2jz';

export const query-2jzConfig: Query-2jzConfig = {
  database: {
    type: 'postgresql',
    connection: process.env.DATABASE_URL
  },
  cache: {
    type: 'redis',
    connection: process.env.REDIS_URL
  },
  models: [
    {
      name: 'User',
      fields: {
        id: { type: 'id' },
        email: { type: 'email', required: true, unique: true },
        password: { type: 'string', required: true },
        firstName: { type: 'string', required: true },
        lastName: { type: 'string', required: true },
        phone: { type: 'string' },
        dateOfBirth: { type: 'date' },
        addresses: { type: 'json', default: [] },
        preferences: { type: 'json', default: {} },
        isActive: { type: 'boolean', default: true },
        isVerified: { type: 'boolean', default: false },
        role: { type: 'enum', values: ['customer', 'admin', 'staff'], default: 'customer' },
        createdAt: { type: 'date', default: 'now' },
        updatedAt: { type: 'date', default: 'now' },
        orders: { type: 'relation', model: 'Order', many: true, foreignKey: 'userId' },
        reviews: { type: 'relation', model: 'Review', many: true, foreignKey: 'userId' }
      },
      indexes: ['email', 'role', 'isActive']
    },
    {
      name: 'Category',
      fields: {
        id: { type: 'id' },
        name: { type: 'string', required: true },
        slug: { type: 'string', required: true, unique: true },
        description: { type: 'string' },
        parentId: { type: 'string' },
        image: { type: 'string' },
        isActive: { type: 'boolean', default: true },
        sortOrder: { type: 'number', default: 0 },
        seo: { type: 'json', default: {} },
        createdAt: { type: 'date', default: 'now' },
        updatedAt: { type: 'date', default: 'now' },
        parent: { type: 'relation', model: 'Category', foreignKey: 'parentId' },
        children: { type: 'relation', model: 'Category', many: true, foreignKey: 'parentId' },
        products: { type: 'relation', model: 'Product', many: true, foreignKey: 'categoryId' }
      },
      indexes: ['slug', 'parentId', 'isActive', 'sortOrder']
    },
    {
      name: 'Product',
      fields: {
        id: { type: 'id' },
        name: { type: 'string', required: true },
        slug: { type: 'string', required: true, unique: true },
        description: { type: 'string', required: true },
        shortDescription: { type: 'string' },
        price: { type: 'number', required: true },
        comparePrice: { type: 'number' },
        sku: { type: 'string', required: true, unique: true },
        barcode: { type: 'string' },
        categoryId: { type: 'string', required: true },
        brand: { type: 'string' },
        weight: { type: 'number' },
        dimensions: { type: 'json' },
        images: { type: 'json', default: [] },
        tags: { type: 'json', default: [] },
        isActive: { type: 'boolean', default: true },
        isDigital: { type: 'boolean', default: false },
        inventory: { type: 'json', default: { quantity: 0, reserved: 0, available: 0 } },
        seo: { type: 'json', default: {} },
        createdAt: { type: 'date', default: 'now' },
        updatedAt: { type: 'date', default: 'now' },
        category: { type: 'relation', model: 'Category', foreignKey: 'categoryId' },
        variants: { type: 'relation', model: 'ProductVariant', many: true, foreignKey: 'productId' },
        reviews: { type: 'relation', model: 'Review', many: true, foreignKey: 'productId' }
      },
      indexes: ['slug', 'sku', 'categoryId', 'isActive', 'price']
    },
    {
      name: 'Order',
      fields: {
        id: { type: 'id' },
        orderNumber: { type: 'string', required: true, unique: true },
        userId: { type: 'string', required: true },
        status: { type: 'enum', values: ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'], default: 'pending' },
        shipping: { type: 'json', required: true },
        billing: { type: 'json', required: true },
        payment: { type: 'json', required: true },
        totals: { type: 'json', required: true },
        notes: { type: 'string' },
        createdAt: { type: 'date', default: 'now' },
        updatedAt: { type: 'date', default: 'now' },
        user: { type: 'relation', model: 'User', foreignKey: 'userId' },
        items: { type: 'relation', model: 'OrderItem', many: true, foreignKey: 'orderId' }
      },
      indexes: ['orderNumber', 'userId', 'status', 'createdAt']
    }
  ]
};

Business Logic

Implement business logic for product management, order processing, and inventory tracking.

Product Service

// src/services/ProductService.ts
import { Query-2jz } from 'query-2jz';
import { Product, ProductVariant } from '../models/Product';

export class ProductService {
  constructor(private query-2jz: Query-2jz) {}

  async getProducts(filters: {
    categoryId?: string;
    search?: string;
    minPrice?: number;
    maxPrice?: number;
    inStock?: boolean;
    limit?: number;
    offset?: number;
  }) {
    const where: any = { isActive: true };
    
    if (filters.categoryId) {
      where.categoryId = filters.categoryId;
    }
    
    if (filters.search) {
      where.$or = [
        { name: { $like: `%${filters.search}%` } },
        { description: { $like: `%${filters.search}%` } },
        { tags: { $contains: filters.search } }
      ];
    }
    
    if (filters.minPrice || filters.maxPrice) {
      where.price = {};
      if (filters.minPrice) where.price.$gte = filters.minPrice;
      if (filters.maxPrice) where.price.$lte = filters.maxPrice;
    }
    
    if (filters.inStock) {
      where['inventory.available'] = { $gt: 0 };
    }

    return this.query-2jz.query({
      model: 'Product',
      where,
      include: ['category', 'variants'],
      orderBy: 'createdAt:desc',
      limit: filters.limit || 20,
      offset: filters.offset || 0
    });
  }

  async getProduct(slug: string) {
    return this.query-2jz.query({
      model: 'Product',
      where: { slug, isActive: true },
      include: ['category', 'variants', 'reviews']
    });
  }

  async updateInventory(productId: string, quantity: number, operation: 'add' | 'subtract' | 'set') {
    const product = await this.query-2jz.query({
      model: 'Product',
      where: { id: productId }
    });

    if (product.data.length === 0) {
      throw new Error('Product not found');
    }

    const currentInventory = product.data[0].inventory;
    let newQuantity: number;

    switch (operation) {
      case 'add':
        newQuantity = currentInventory.quantity + quantity;
        break;
      case 'subtract':
        newQuantity = Math.max(0, currentInventory.quantity - quantity);
        break;
      case 'set':
        newQuantity = quantity;
        break;
    }

    const available = Math.max(0, newQuantity - currentInventory.reserved);

    return this.query-2jz.update({
      model: 'Product',
      id: productId,
      data: {
        inventory: {
          quantity: newQuantity,
          reserved: currentInventory.reserved,
          available
        }
      }
    });
  }

  async reserveInventory(productId: string, quantity: number) {
    const product = await this.query-2jz.query({
      model: 'Product',
      where: { id: productId }
    });

    if (product.data.length === 0) {
      throw new Error('Product not found');
    }

    const currentInventory = product.data[0].inventory;
    
    if (currentInventory.available < quantity) {
      throw new Error('Insufficient inventory');
    }

    const newReserved = currentInventory.reserved + quantity;
    const newAvailable = currentInventory.quantity - newReserved;

    return this.query-2jz.update({
      model: 'Product',
      id: productId,
      data: {
        inventory: {
          quantity: currentInventory.quantity,
          reserved: newReserved,
          available: newAvailable
        }
      }
    });
  }
}

Order Service

// src/services/OrderService.ts
import { Query-2jz } from 'query-2jz';
import { Order, OrderItem } from '../models/Order';
import { ProductService } from './ProductService';

export class OrderService {
  constructor(
    private query-2jz: Query-2jz,
    private productService: ProductService
  ) {}

  async createOrder(userId: string, items: Array<{
    productId: string;
    variantId?: string;
    quantity: number;
  }>) {
    // Calculate totals
    let subtotal = 0;
    const orderItems = [];

    for (const item of items) {
      const product = await this.query-2jz.query({
        model: 'Product',
        where: { id: item.productId }
      });

      if (product.data.length === 0) {
        throw new Error(`Product ${item.productId} not found`);
      }

      const productData = product.data[0];
      const price = productData.price;
      const total = price * item.quantity;
      
      subtotal += total;

      orderItems.push({
        productId: item.productId,
        variantId: item.variantId,
        quantity: item.quantity,
        price,
        total
      });

      // Reserve inventory
      await this.productService.reserveInventory(item.productId, item.quantity);
    }

    // Calculate taxes and shipping (simplified)
    const tax = subtotal * 0.08; // 8% tax
    const shipping = subtotal > 100 ? 0 : 10; // Free shipping over $100
    const total = subtotal + tax + shipping;

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

    // Create order
    const order = await this.query-2jz.create({
      model: 'Order',
      data: {
        orderNumber,
        userId,
        status: 'pending',
        shipping: {
          address: {}, // Get from user profile
          method: 'standard',
          cost: shipping
        },
        billing: {
          address: {}, // Get from user profile
          method: 'credit_card'
        },
        payment: {
          method: 'credit_card',
          status: 'pending',
          amount: total,
          currency: 'USD'
        },
        totals: {
          subtotal,
          tax,
          shipping,
          discount: 0,
          total
        }
      }
    });

    // Create order items
    for (const item of orderItems) {
      await this.query-2jz.create({
        model: 'OrderItem',
        data: {
          orderId: order.id,
          ...item
        }
      });
    }

    return order;
  }

  async updateOrderStatus(orderId: string, status: string) {
    const order = await this.query-2jz.query({
      model: 'Order',
      where: { id: orderId }
    });

    if (order.data.length === 0) {
      throw new Error('Order not found');
    }

    const updatedOrder = await this.query-2jz.update({
      model: 'Order',
      id: orderId,
      data: { status }
    });

    // If order is cancelled or refunded, release reserved inventory
    if (status === 'cancelled' || status === 'refunded') {
      const items = await this.query-2jz.query({
        model: 'OrderItem',
        where: { orderId }
      });

      for (const item of items.data) {
        await this.productService.updateInventory(
          item.productId,
          item.quantity,
          'add'
        );
      }
    }

    return updatedOrder;
  }

  async getOrderHistory(userId: string, limit = 20, offset = 0) {
    return this.query-2jz.query({
      model: 'Order',
      where: { userId },
      include: ['items', 'items.product'],
      orderBy: 'createdAt:desc',
      limit,
      offset
    });
  }
}

API Controllers

Create REST API controllers for the e-commerce application.

Product Controller

// src/controllers/ProductController.ts
import { Request, Response } from 'express';
import { ProductService } from '../services/ProductService';

export class ProductController {
  constructor(private productService: ProductService) {}

  async getProducts(req: Request, res: Response) {
    try {
      const {
        categoryId,
        search,
        minPrice,
        maxPrice,
        inStock,
        limit = 20,
        offset = 0
      } = req.query;

      const products = await this.productService.getProducts({
        categoryId: categoryId as string,
        search: search as string,
        minPrice: minPrice ? parseFloat(minPrice as string) : undefined,
        maxPrice: maxPrice ? parseFloat(maxPrice as string) : undefined,
        inStock: inStock === 'true',
        limit: parseInt(limit as string),
        offset: parseInt(offset as string)
      });

      res.json(products);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async getProduct(req: Request, res: Response) {
    try {
      const { slug } = req.params;
      const product = await this.productService.getProduct(slug);
      
      if (product.data.length === 0) {
        return res.status(404).json({ error: 'Product not found' });
      }

      res.json(product.data[0]);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async getCategories(req: Request, res: Response) {
    try {
      const categories = await this.query-2jz.query({
        model: 'Category',
        where: { isActive: true },
        orderBy: 'sortOrder:asc'
      });

      res.json(categories.data);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

Order Controller

// src/controllers/OrderController.ts
import { Request, Response } from 'express';
import { OrderService } from '../services/OrderService';

export class OrderController {
  constructor(private orderService: OrderService) {}

  async createOrder(req: Request, res: Response) {
    try {
      const userId = req.user.id;
      const { items } = req.body;

      if (!items || !Array.isArray(items) || items.length === 0) {
        return res.status(400).json({ error: 'Items are required' });
      }

      const order = await this.orderService.createOrder(userId, items);
      res.status(201).json(order);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async getOrderHistory(req: Request, res: Response) {
    try {
      const userId = req.user.id;
      const { limit = 20, offset = 0 } = req.query;

      const orders = await this.orderService.getOrderHistory(
        userId,
        parseInt(limit as string),
        parseInt(offset as string)
      );

      res.json(orders);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async getOrder(req: Request, res: Response) {
    try {
      const { id } = req.params;
      const userId = req.user.id;

      const order = await this.query-2jz.query({
        model: 'Order',
        where: { id, userId },
        include: ['items', 'items.product']
      });

      if (order.data.length === 0) {
        return res.status(404).json({ error: 'Order not found' });
      }

      res.json(order.data[0]);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }

  async updateOrderStatus(req: Request, res: Response) {
    try {
      const { id } = req.params;
      const { status } = req.body;

      const order = await this.orderService.updateOrderStatus(id, status);
      res.json(order);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

Real-time Features

Add real-time notifications for order updates and inventory changes.

// src/services/NotificationService.ts
import { Query-2jz } from 'query-2jz';

export class NotificationService {
  constructor(private query-2jz: Query-2jz) {}

  async notifyOrderUpdate(orderId: string, status: string) {
    const order = await this.query-2jz.query({
      model: 'Order',
      where: { id: orderId },
      include: ['user']
    });

    if (order.data.length === 0) {
      return;
    }

    const orderData = order.data[0];
    const message = `Your order ${orderData.orderNumber} status has been updated to ${status}`;

    // Send real-time notification
    this.query-2jz.publish('order-update', {
      userId: orderData.userId,
      orderId,
      status,
      message
    });

    // Create notification record
    await this.query-2jz.create({
      model: 'Notification',
      data: {
        userId: orderData.userId,
        type: 'order-update',
        title: 'Order Status Update',
        message,
        data: { orderId, status },
        isRead: false
      }
    });
  }

  async notifyLowInventory(productId: string) {
    const product = await this.query-2jz.query({
      model: 'Product',
      where: { id: productId }
    });

    if (product.data.length === 0) {
      return;
    }

    const productData = product.data[0];
    const message = `Product ${productData.name} is running low on inventory`;

    // Notify admins
    const admins = await this.query-2jz.query({
      model: 'User',
      where: { role: 'admin' }
    });

    for (const admin of admins.data) {
      this.query-2jz.publish('inventory-alert', {
        userId: admin.id,
        productId,
        message
      });

      await this.query-2jz.create({
        model: 'Notification',
        data: {
          userId: admin.id,
          type: 'inventory-alert',
          title: 'Low Inventory Alert',
          message,
          data: { productId },
          isRead: false
        }
      });
    }
  }
}

Next Steps

Enhance your e-commerce application with additional features.

Advanced Features

  • • Payment gateway integration (Stripe, PayPal)
  • • Email notifications and receipts
  • • Advanced search and filtering
  • • Product recommendations
  • • Wishlist and favorites
  • • Customer reviews and ratings
  • • Admin dashboard and analytics