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.mdData 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