@happyvertical/smrt-products
Product catalog and inventory management with variants, pricing, and stock tracking.
Overview
smrt-products is a comprehensive product catalog management system built on the SMRT framework. It provides a complete solution for managing products, categories, inventory, and specifications with auto-generated REST APIs, MCP tools for AI integration, and reactive Svelte 5 components.
The module serves three purposes: (1) standalone application with its own dev server, (2) federated modules that can be consumed by other micro-frontends, and (3) NPM package for direct imports. All three consumption modes use the same source code, ensuring consistency and maintainability.
Architecture:
┌─────────────────────────────────────────┐ │ smrt-products Module │ ├─────────────────────────────────────────┤ │ Models (Decorated with @smrt) │ │ • Product (specifications, tags, price) │ │ • Category (hierarchical, counts) │ ├─────────────────────────────────────────┤ │ Auto-Generated (Vite Plugin) │ │ • REST APIs │ │ • TypeScript Client │ │ • MCP Tools │ │ • Type Definitions │ ├─────────────────────────────────────────┤ │ UI Components (Svelte 5) │ │ • ProductCard, ProductForm │ │ • ProductCatalog, CategoryManager │ │ • Stores with runes │ ├─────────────────────────────────────────┤ │ Consumption Modes │ │ • Standalone: npm run dev:standalone │ │ • Federation: npm run dev:federation │ │ • Library: npm package imports │ └─────────────────────────────────────────┘
Installation
# Using pnpm (recommended)
pnpm add @happyvertical/smrt-products
# Using npm
npm install @happyvertical/smrt-productsThe module depends on @happyvertical/smrt-core for base classes, @happyvertical/ai for AI operations, and @happyvertical/sql for database operations.
Quick Start (5 Minutes)
1. Initialize Product Store
import { ProductStoreClass } from '@happyvertical/smrt-products';
// Create singleton store instance
const productStore = new ProductStoreClass();
// Load products from API
await productStore.loadProducts();
console.log('Products loaded:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);2. Display Product Catalog
<script lang="ts">
import { ProductCatalog } from '@happyvertical/smrt-products';
</script>
<!-- Complete catalog with search, filtering, CRUD -->
<ProductCatalog
showCreateForm={true}
readonly={false}
/>
<!-- Auto-displays: search, category filter, stats, product grid -->
<!-- Auto-handles: create, edit, delete, loading, error states -->3. Create Product with Form
<script lang="ts">
import { ProductForm } from '@happyvertical/smrt-products';
let isSaving = false;
async function handleSubmit(product) {
isSaving = true;
const result = await productStore.createProduct(product);
if (result.success) {
console.log('Product created:', result.data);
} else {
console.error('Error:', result.error);
}
isSaving = false;
}
</script>
<ProductForm
onSubmit={handleSubmit}
loading={isSaving}
/>
<!-- Form includes: name, description, price, category, tags, inStock checkbox -->
<!-- Features: client-side validation, error display, loading state -->4. Search and Filter Products
// Search by text (name, description, tags)
const results = productStore.searchProducts('laptop');
// Filter by category
const electronics = productStore.filterByCategory('Electronics');
// Filter in-stock only
const available = productStore.filterInStock();
// Access derived state
console.log('Total products:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);
console.log('Categories:', productStore.categories);Core Concepts
Product Model Structure
The Product class extends SmrtObject with comprehensive fields for
e-commerce and inventory management:
| Field | Type | Purpose | Example |
|---|---|---|---|
name | string | Product identifier | "USB-C Hub" |
description | string | Detailed info | "7-port hub with PD..." |
category | string | Category name | "Electronics" |
manufacturer | string | Maker/brand | "TechCorp" |
model | string | Model number | "TC-HUB-7P" |
price | number | Decimal price | 39.99 |
inStock | boolean | Availability | true |
specifications | Record | Extensible attributes | { weight: "1kg", ports: 7 } |
tags | string[] | Searchable keywords | ["usb", "hub", "adapter"] |
Category System
Categories form a hierarchical structure for organizing products:
class Category extends SmrtObject {
name: string
description: string
parentId: string | null // Self-referencing hierarchy
level: number // Depth in hierarchy (0 = root)
productCount: number // Cached count for performance
active: boolean // Soft delete flag
// Methods
async getProducts(): Promise<Product[]>
async getSubcategories(): Promise<Category[]>
async updateProductCount(): Promise<void>
static async getRootCategories(): Promise<Category[]>
}Specifications System
Products have an extensible specifications field for storing arbitrary attributes:
// Store specifications
await product.updateSpecification('weight', '1.2kg');
await product.updateSpecification('warranty', 24); // months
await product.updateSpecification('colors', ['black', 'silver', 'gold']);
// Retrieve specifications
const weight = await product.getSpecification('weight');
const warranty = await product.getSpecification('warranty');
// Example specifications by product type:
// Electronics: weight, dimensions, power, warranty
// Clothing: size, material, care_instructions, colors
// Furniture: dimensions, weight_capacity, assembly_required, materialsReactive Store (Svelte 5 Runes)
The ProductStoreClass uses Svelte 5's runes for reactive state management:
export class ProductStoreClass {
// Reactive state with $state rune
items = $state<ProductData[]>([]);
loading = $state(false);
error = $state<string | null>(null);
selectedProduct = $state<ProductData | null>(null);
// Derived state with $derived rune
inStockCount = $derived(
this.items.filter(p => p.inStock).length
);
totalValue = $derived(
this.items.reduce((sum, p) => sum + (p.price || 0), 0)
);
categories = $derived(
[...new Set(this.items.map(p => p.category).filter(Boolean))]
);
// Actions
async loadProducts(): Promise<void>
async createProduct(data): Promise<ApiResponse>
async updateProduct(id, updates): Promise<ApiResponse>
async deleteProduct(id): Promise<void>
// Filters (return derived arrays)
filterByCategory(category): ProductData[]
filterInStock(): ProductData[]
searchProducts(query): ProductData[]
}API Reference
Auto-Generated REST APIs
The @smrt decorator automatically generates REST endpoints:
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/v1/products | List all products |
POST | /api/v1/products | Create new product |
GET | /api/v1/products/:id | Get single product |
PUT | /api/v1/products/:id | Update product |
GET | /api/v1/categories | List all categories |
TypeScript Client
import { createClient } from '@happyvertical/smrt-products/client';
const client = createClient('/api/v1');
// List products
const products = await client.products.list();
// Get single product
const product = await client.products.get(productId);
// Create product
const newProduct = await client.products.create({
name: 'New Product',
price: 29.99,
inStock: true
});
// Update product
const updated = await client.products.update(productId, {
price: 24.99,
inStock: false
});Components API
ProductCard
<ProductCard
product={productData}
onEdit={handleEdit}
onDelete={handleDelete}
/>
<!-- Props:
- product: ProductData (required)
- onEdit?: (product: ProductData) => void
- onDelete?: (product: ProductData) => void
Displays: name, manufacturer, model, category, tags
Actions: Edit/Delete buttons (if handlers provided)
-->ProductForm
<ProductForm
product={existingProduct}
onSubmit={handleSubmit}
onCancel={handleCancel}
loading={isSaving}
/>
<!-- Props:
- product?: ProductData (optional, for editing)
- onSubmit: (product: Partial<ProductData>) => void | Promise<void>
- onCancel?: () => void
- loading?: boolean
Fields: name (required), description, price (required, non-negative),
category, tags (comma-separated), inStock (checkbox)
Features: client-side validation, error display, loading state
-->ProductCatalog
<ProductCatalog
readonly={false}
showCreateForm={true}
/>
<!-- Props:
- readonly?: boolean (default: false)
- showCreateForm?: boolean (default: false)
Features:
- Search bar (filters name, description, tags)
- Category dropdown filter
- Stats display (total, in stock, total value)
- Product grid with ProductCard components
- Create/Edit/Delete operations
- Loading and error states
-->Tutorials
Tutorial 1: Creating and Managing Your Product Catalog (45-60 min)
Build a complete product catalog from scratch:
- Initialize ProductStore singleton in your app
- Create first product using ProductForm component
- Display catalog with ProductCatalog component
- Implement add/edit/delete operations
- Handle error states gracefully
- Deploy to production with persistence
Tutorial 2: Advanced Product Organization with Categories (45-60 min)
Organize products with hierarchical categories:
- Set up hierarchical category structure (Electronics > Accessories > Cables)
- Assign products to categories
- Display category-filtered product views
- Implement category-specific landing pages
- Calculate and display product counts per category
- Build category navigation menu
Tutorial 3: Product Search and Filtering (30-45 min)
Implement advanced search and filtering:
- Full-text search across name, description, and tags
- Category filters (single and multi-select)
- Availability filter (in-stock only checkbox)
- Combine multiple filters for precise results
- Display result counts and stats
- Clear all filters button
Tutorial 4: Specifications and Product Variants (45-60 min)
Handle product variants using specifications:
- Model product variants using specifications field
- Store variant combinations (size, color, material)
- Display variant selector UI in ProductCard
- Update inventory per variant
- Support dynamic specification schema per category
- Validate specification values before saving
Real-World Examples
Example 1: E-Commerce Product Catalog
// Initialize store
const store = new ProductStoreClass();
await store.loadProducts();
// Create category
const electronics = await Category.create({
name: 'Electronics',
description: 'Electronic devices and accessories',
level: 0
});
// Create product in category
const hub = await store.createProduct({
name: 'USB-C Hub',
description: '7-port hub with 100W Power Delivery',
category: 'Electronics',
manufacturer: 'TechCorp',
model: 'TC-HUB-7P',
price: 39.99,
inStock: true,
tags: ['usb', 'hub', 'adapter', 'usb-c'],
specifications: {
ports: 7,
power_delivery: '100W',
weight: '120g',
warranty: 24
}
});
// Search and filter
const hubs = store.searchProducts('hub');
const available = store.filterInStock();
const electronicsProducts = store.filterByCategory('Electronics');
console.log('Total products:', store.items.length);
console.log('In stock:', store.inStockCount);
console.log('Catalog value:', store.totalValue);Example 2: Product Inventory Dashboard
<script lang="ts">
import { ProductCatalog } from '@happyvertical/smrt-products';
import { onMount } from 'svelte';
onMount(() => {
// Catalog auto-loads products on mount
});
</script>
<div class="dashboard">
<h1>Inventory Management</h1>
<ProductCatalog
showCreateForm={true}
readonly={false}
/>
<!-- Auto-displays:
- Search bar (filters by text)
- Category filter dropdown
- Stats: total products, in stock, total value
- Product grid with cards
- Create/Edit/Delete operations
- Loading and error states
-->
</div>
<style>
.dashboard {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
</style>Example 3: Admin Product Editor
<script lang="ts">
import { ProductForm } from '@happyvertical/smrt-products';
import { productStore } from './stores/product-store';
let selectedProduct = productStore.selectedProduct;
let isSaving = false;
async function handleSave(productData) {
isSaving = true;
try {
if (selectedProduct?.id) {
// Update existing
await productStore.updateProduct(selectedProduct.id, productData);
} else {
// Create new
await productStore.createProduct(productData);
}
handleCancel();
} catch (error) {
console.error('Save failed:', error);
} finally {
isSaving = false;
}
}
function handleCancel() {
productStore.selectProduct(null);
}
</script>
<div class="editor">
<h2>{selectedProduct ? 'Edit' : 'Create'} Product</h2>
<ProductForm
product={selectedProduct}
onSubmit={handleSave}
onCancel={handleCancel}
loading={isSaving}
/>
</div>Example 4: Manufacturer-Based Search
import { Product } from '@happyvertical/smrt-products';
// Find all products by manufacturer
const techCorpProducts = await Product.findByManufacturer('TechCorp');
console.log('Found', techCorpProducts.length, 'TechCorp products');
// Compare specifications across products
for (const product of techCorpProducts) {
const warranty = await product.getSpecification('warranty');
const weight = await product.getSpecification('weight');
console.log(product.name);
console.log(' Price:', product.price);
console.log(' Warranty:', warranty, 'months');
console.log(' Weight:', weight);
console.log(' In stock:', product.inStock);
console.log('---');
}
// Calculate average price
const avgPrice = techCorpProducts.reduce((sum, p) => sum + p.price, 0) / techCorpProducts.length;
console.log('Average TechCorp product price:', avgPrice.toFixed(2));Example 5: Product Specifications System
// Create product with specifications
const laptop = await Product.create({
name: 'Pro Laptop 15"',
manufacturer: 'TechCorp',
model: 'PL-15-2024',
price: 1299.99,
inStock: true,
category: 'Computers',
specifications: {
screen_size: '15.6 inches',
resolution: '1920x1080',
processor: 'Intel Core i7',
ram: '16GB',
storage: '512GB SSD',
weight: '1.8kg',
battery_life: '10 hours',
warranty: 36
}
});
// Update specifications
await laptop.updateSpecification('ram', '32GB');
await laptop.updateSpecification('storage', '1TB SSD');
// Retrieve specifications
const ram = await laptop.getSpecification('ram');
const processor = await laptop.getSpecification('processor');
console.log('Upgraded RAM:', ram);
console.log('Processor:', processor);
// Search products with specific specification
const allLaptops = await Product.findByCategory('Computers');
const highRamLaptops = [];
for (const product of allLaptops) {
const ram = await product.getSpecification('ram');
if (ram && parseInt(ram) >= 32) {
highRamLaptops.push(product);
}
}
console.log('Laptops with 32GB+ RAM:', highRamLaptops.length);Integration Patterns
With smrt-core
- Product and Category extend
SmrtObjectfor ORM/persistence @smrtdecorator auto-generates REST APIs, CLI commands, MCP tools- Inherits AI methods:
is(),do(),describe() - Database schema auto-generated from TypeScript types
- Built-in validation and lifecycle hooks
With smrt-commerce
- Products feed into shopping cart and checkout
- Orders reference products via foreign keys
- Inventory sync with order transactions
- Price updates trigger commerce events
- Product availability affects checkout flow
With smrt-assets
- Product images/videos managed as assets
- ALT text auto-generated via AI for accessibility
- Asset versions for product photo updates
- Asset tagging aligns with product tags
- Derivatives (thumbnails, webp) generated automatically
With smrt-ledgers
- Product prices feed into accounting entries
- Revenue recognized per product/category
- Cost of goods sold tracking
- Inventory valuation for balance sheet
- Tax calculations based on product category
With smrt-tenancy
- Products isolated per tenant automatically
- Category hierarchies per tenant
- Separate product inventories
- Tenant-specific pricing and visibility
- Shared schema, separate data
Module Federation
- ProductCard exported for consumption in other micro-frontends
- ProductCatalog embeddable in dashboards
- Product model definitions shared across services
- ProductStore accessible in federated apps
- Type safety maintained across boundaries
Best Practices
DOs
- Use categories for organization (don't rely on tags alone)
- Cache derived data (inStockCount, totalValue) via store
- Validate product data before submission (form handles this)
- Use tags for searchable keywords and filtering
- Keep specifications schema consistent per product type
- Lazy-load products for large catalogs (implement pagination)
- Use the store for reactive state (not direct API calls)
- Implement pagination for large product lists (1000+ items)
- Use STI pattern for product variants/types
DON'Ts
- Don't store large files in specifications (use smrt-assets)
- Don't create deep category hierarchies (> 5 levels)
- Don't hard-code prices (use dynamic fields)
- Don't skip validation on forms
- Don't duplicate product data across services (use federation)
- Don't mutate store state directly (use provided actions)
- Don't fetch all products on page load without pagination
- Don't leave error states unhandled in UI
Common Issues and Troubleshooting
Issue: Products not appearing in catalog
Cause: Store not initialized or loadProducts() not called
Solution: Call loadProducts() in onMount or component initialization.
onMount(() => {
productStore.loadProducts();
});Issue: Form validation errors
Cause: Required fields empty (name, price) or invalid values
Solution: ProductForm component handles validation. Check console for errors. Ensure name is non-empty and price is a positive number.
Issue: Specifications not saving
Cause: Using wrong method or not awaiting promise
Solution: Always await updateSpecification(), pass correct key/value types.
// Correct
await product.updateSpecification('weight', '1.2kg');
// Wrong: not awaiting
product.updateSpecification('weight', '1.2kg'); // Promise not resolvedIssue: Search not finding results
Cause: Search only looks at name, description, and tags
Solution: Add relevant keywords to tags array for better searchability.
Issue: Category hierarchy confusion
Cause: Not setting parentId correctly or level incorrect
Solution: Use getRootCategories() for level=0. Set parentId for subcategories.
Issue: Performance with large catalogs
Cause: Loading all products at once without pagination
Solution: Implement API pagination. Filter/search client-side for small datasets (<1000 items), server-side for larger.
Issue: Module federation components not loading
Cause: Expose config not exporting components
Solution: Verify federation/expose.config.ts includes ProductCard, ProductCatalog exports.