Build a Product Catalog with Filtering
In this tutorial, you will build a product catalog with faceted navigation. This is a common pattern for e-commerce sites where users filter products by category, price, brand, and other attributes.
Time required: 20 minutes
What you will build: A product catalog that supports filtering by category, price range, and brand, with dynamic facet counts that update as users apply filters.
Prerequisites
- Completed the Build Your First dcupl Application tutorial
- Node.js 18 or higher installed
- Basic TypeScript/JavaScript knowledge
Step 1: Set Up the Project
Create a new project directory and install dependencies:
mkdir product-catalog
cd product-catalog
npm init -y
npm install @dcupl/coreCreate the main application file:
import { Dcupl } from '@dcupl/core';
async function main() {
const dcupl = new Dcupl();
// We will build the catalog here
}
main();Step 2: Define the Product Model
A product catalog needs a model that captures all filterable attributes. Define a comprehensive product model:
import { Dcupl } from '@dcupl/core';
async function main() {
const dcupl = new Dcupl();
// Define the Product model with filterable properties
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'description', type: 'string' },
{ key: 'category', type: 'string' },
{ key: 'brand', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'rating', type: 'float' },
{ key: 'inStock', type: 'boolean' },
{ key: 'tags', type: 'Array<string>' },
],
});
console.log('Product model defined');
}
main();Notice the tags property uses Array type. This allows products to have multiple tags like "new", "sale", "featured".
Step 3: Load Product Data
Add a realistic product dataset. In production, this data would come from an API or database:
// ... after dcupl.models.set() ...
// Load product data
dcupl.data.set(
[
// Electronics
{
key: 'p1',
name: 'Laptop Pro 15',
description: 'High-performance laptop',
category: 'Electronics',
brand: 'TechCorp',
price: 1299.99,
rating: 4.8,
inStock: true,
tags: ['featured', 'new'],
},
{
key: 'p2',
name: 'Wireless Mouse',
description: 'Ergonomic wireless mouse',
category: 'Electronics',
brand: 'TechCorp',
price: 49.99,
rating: 4.5,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p3',
name: 'USB-C Hub',
description: '7-in-1 USB-C hub',
category: 'Electronics',
brand: 'ConnectPro',
price: 79.99,
rating: 4.2,
inStock: true,
tags: [],
},
{
key: 'p4',
name: 'Mechanical Keyboard',
description: 'RGB mechanical keyboard',
category: 'Electronics',
brand: 'TypeMaster',
price: 149.99,
rating: 4.7,
inStock: false,
tags: ['popular'],
},
{
key: 'p5',
name: 'Monitor 27"',
description: '4K IPS monitor',
category: 'Electronics',
brand: 'ViewTech',
price: 449.99,
rating: 4.6,
inStock: true,
tags: ['featured'],
},
// Clothing
{
key: 'p6',
name: 'Cotton T-Shirt',
description: '100% organic cotton',
category: 'Clothing',
brand: 'EcoWear',
price: 29.99,
rating: 4.4,
inStock: true,
tags: ['sale', 'eco-friendly'],
},
{
key: 'p7',
name: 'Denim Jeans',
description: 'Classic fit jeans',
category: 'Clothing',
brand: 'DenimCo',
price: 79.99,
rating: 4.3,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p8',
name: 'Running Shoes',
description: 'Lightweight running shoes',
category: 'Clothing',
brand: 'SportyFeet',
price: 129.99,
rating: 4.8,
inStock: true,
tags: ['new', 'popular'],
},
{
key: 'p9',
name: 'Winter Jacket',
description: 'Waterproof winter jacket',
category: 'Clothing',
brand: 'OutdoorGear',
price: 199.99,
rating: 4.5,
inStock: false,
tags: ['seasonal'],
},
{
key: 'p10',
name: 'Wool Sweater',
description: 'Merino wool sweater',
category: 'Clothing',
brand: 'EcoWear',
price: 89.99,
rating: 4.6,
inStock: true,
tags: ['eco-friendly'],
},
// Home & Garden
{
key: 'p11',
name: 'Coffee Maker',
description: 'Programmable coffee maker',
category: 'Home & Garden',
brand: 'BrewMaster',
price: 99.99,
rating: 4.4,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p12',
name: 'Plant Pot Set',
description: 'Ceramic pot set of 3',
category: 'Home & Garden',
brand: 'GreenThumb',
price: 34.99,
rating: 4.1,
inStock: true,
tags: ['sale'],
},
{
key: 'p13',
name: 'LED Desk Lamp',
description: 'Adjustable LED lamp',
category: 'Home & Garden',
brand: 'LightWorks',
price: 59.99,
rating: 4.7,
inStock: true,
tags: ['featured'],
},
{
key: 'p14',
name: 'Air Purifier',
description: 'HEPA air purifier',
category: 'Home & Garden',
brand: 'CleanAir',
price: 249.99,
rating: 4.5,
inStock: true,
tags: ['new'],
},
{
key: 'p15',
name: 'Kitchen Scale',
description: 'Digital kitchen scale',
category: 'Home & Garden',
brand: 'CookPro',
price: 24.99,
rating: 4.2,
inStock: true,
tags: [],
},
],
{ model: 'Product' }
);
await dcupl.init();
console.log('Product catalog initialized with', 15, 'products');Step 4: Create the Product List
Create a list that you will use for all queries:
// ... after dcupl.init() ...
// Create the main product list
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Display initial stats
console.log('\n--- Catalog Overview ---');
console.log('Total products:', productList.catalog.query.count());Step 5: Build Faceted Navigation
Facets are the counts shown next to filter options. For example, "Electronics (5)" tells users there are 5 electronics products.
// Get facets for all filterable attributes
function displayFacets() {
console.log('\n--- Available Filters ---');
// Category facets
console.log('\nCategories:');
productList.catalog.fn.facets({ attribute: 'category' }).forEach((f) => {
console.log(` [ ] ${f.value} (${f.count})`);
});
// Brand facets
console.log('\nBrands:');
productList.catalog.fn.facets({ attribute: 'brand' }).forEach((f) => {
console.log(` [ ] ${f.value} (${f.count})`);
});
// In-stock facet
console.log('\nAvailability:');
productList.catalog.fn.facets({ attribute: 'inStock' }).forEach((f) => {
const label = f.value === true ? 'In Stock' : 'Out of Stock';
console.log(` [ ] ${label} (${f.count})`);
});
// Tags facets
console.log('\nTags:');
productList.catalog.fn.facets({ attribute: 'tags' }).forEach((f) => {
if (f.value) {
console.log(` [ ] ${f.value} (${f.count})`);
}
});
}
displayFacets();Run the app to see the facet counts:
npx tsx catalog.tsStep 6: Filter by Category
Now simulate a user selecting "Electronics":
// User selects "Electronics" category
console.log('\n--- User selects: Electronics ---');
productList.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
// Show filtered products
console.log('\nFiltered products:');
productList.catalog.query.items().forEach((p) => {
console.log(` ${p.name} - ${p.price} (${p.brand})`);
});
// Show how facets update for other attributes
console.log('\nBrands in Electronics:');
productList.catalog.fn.facets({ attribute: 'brand' }).forEach((f) => {
console.log(` [ ] ${f.value} (${f.count})`);
});Notice how brand facets now only count electronics products.
Step 7: Add Price Range Filter
Add a price range filter on top of the category filter:
// User adds price filter: $50 - $200
console.log('\n--- User adds: Price $50 - $200 ---');
productList.catalog.query.addCondition({
attribute: 'price',
operator: 'gte',
value: 50,
});
productList.catalog.query.addCondition({
attribute: 'price',
operator: 'lte',
value: 200,
});
console.log('\nFiltered products (Electronics, $50-$200):');
productList.catalog.query.items().forEach((p) => {
console.log(` ${p.name} - ${p.price}`);
});
console.log('\nMatching count:', productList.catalog.query.count());Step 8: Filter by Multiple Values
Allow users to select multiple brands. First, clear filters and start fresh:
// Clear all filters
productList.catalog.query.clear();
console.log('\n--- New search: Multiple brands ---');
// Filter to show products from TechCorp OR EcoWear
// Use 'in' operator for multiple values
productList.catalog.query.addCondition({
attribute: 'brand',
operator: 'in',
value: ['TechCorp', 'EcoWear'],
});
console.log('\nProducts from TechCorp or EcoWear:');
productList.catalog.query.items().forEach((p) => {
console.log(` ${p.name} - ${p.brand}`);
});Step 9: Search by Tags
Filter products that have specific tags:
productList.catalog.query.clear();
console.log('\n--- Filter by tag: featured ---');
productList.catalog.query.addCondition({
attribute: 'tags',
operator: 'eq',
value: 'featured',
});
console.log('\nFeatured products:');
productList.catalog.query.items().forEach((p) => {
console.log(` ${p.name} - ${p.price}`);
});Step 10: Get Price Statistics
Show price range and average for filtered results:
productList.catalog.query.clear();
// Filter to Electronics
productList.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
// Get price statistics
const priceStats = productList.catalog.fn.aggregate({
attribute: 'price',
});
console.log('\n--- Electronics Price Statistics ---');
console.log(` Min: ${priceStats.min.toFixed(2)}`);
console.log(` Max: ${priceStats.max.toFixed(2)}`);
console.log(` Average: ${priceStats.avg.toFixed(2)}`);Step 11: Sort Products
Add sorting options for the catalog:
productList.catalog.query.clear();
console.log('\n--- Sorting Examples ---');
// Sort by price (low to high)
console.log('\nBy price (low to high):');
productList.catalog.query
.items({
sort: { attributes: ['price'], order: ['asc'] },
count: 5,
})
.forEach((p) => {
console.log(` ${p.price} - ${p.name}`);
});
// Sort by rating (high to low)
console.log('\nBy rating (high to low):');
productList.catalog.query
.items({
sort: { attributes: ['rating'], order: ['desc'] },
count: 5,
})
.forEach((p) => {
console.log(` ${p.rating} stars - ${p.name}`);
});
// Sort by multiple attributes: category, then price
console.log('\nBy category, then price:');
productList.catalog.query
.items({
sort: { attributes: ['category', 'price'], order: ['asc', 'asc'] },
})
.forEach((p) => {
console.log(` ${p.category} | ${p.price} - ${p.name}`);
});Step 12: Combine Everything
Here is a realistic filter scenario combining multiple conditions:
productList.catalog.query.clear();
console.log('\n--- Complete Filter Example ---');
console.log('Filters: Clothing OR Home & Garden, In Stock, Under $100');
// Multiple categories
productList.catalog.query.addCondition({
attribute: 'category',
operator: 'in',
value: ['Clothing', 'Home & Garden'],
});
// Must be in stock
productList.catalog.query.addCondition({
attribute: 'inStock',
operator: 'eq',
value: true,
});
// Price under $100
productList.catalog.query.addCondition({
attribute: 'price',
operator: 'lt',
value: 100,
});
// Display results with sorting
const results = productList.catalog.query.items({
sort: { attributes: ['price'], order: ['asc'] },
});
console.log(`\nFound ${results.length} products:\n`);
results.forEach((p) => {
console.log(` ${p.name}`);
console.log(` Category: ${p.category} | Brand: ${p.brand}`);
console.log(` Price: ${p.price} | Rating: ${p.rating} stars`);
console.log('');
});
// Show remaining filter options
console.log('Brands available in filtered results:');
productList.catalog.fn.facets({ attribute: 'brand' }).forEach((f) => {
console.log(` [ ] ${f.value} (${f.count})`);
});Complete Code
Here is the complete product catalog application:
import { Dcupl } from '@dcupl/core';
async function main() {
const dcupl = new Dcupl();
// Define Product model
dcupl.models.set({
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'description', type: 'string' },
{ key: 'category', type: 'string' },
{ key: 'brand', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'rating', type: 'float' },
{ key: 'inStock', type: 'boolean' },
{ key: 'tags', type: 'Array<string>' },
],
});
// Load product data
dcupl.data.set(
[
{
key: 'p1',
name: 'Laptop Pro 15',
description: 'High-performance laptop',
category: 'Electronics',
brand: 'TechCorp',
price: 1299.99,
rating: 4.8,
inStock: true,
tags: ['featured', 'new'],
},
{
key: 'p2',
name: 'Wireless Mouse',
description: 'Ergonomic wireless mouse',
category: 'Electronics',
brand: 'TechCorp',
price: 49.99,
rating: 4.5,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p3',
name: 'USB-C Hub',
description: '7-in-1 USB-C hub',
category: 'Electronics',
brand: 'ConnectPro',
price: 79.99,
rating: 4.2,
inStock: true,
tags: [],
},
{
key: 'p4',
name: 'Mechanical Keyboard',
description: 'RGB mechanical keyboard',
category: 'Electronics',
brand: 'TypeMaster',
price: 149.99,
rating: 4.7,
inStock: false,
tags: ['popular'],
},
{
key: 'p5',
name: 'Monitor 27"',
description: '4K IPS monitor',
category: 'Electronics',
brand: 'ViewTech',
price: 449.99,
rating: 4.6,
inStock: true,
tags: ['featured'],
},
{
key: 'p6',
name: 'Cotton T-Shirt',
description: '100% organic cotton',
category: 'Clothing',
brand: 'EcoWear',
price: 29.99,
rating: 4.4,
inStock: true,
tags: ['sale', 'eco-friendly'],
},
{
key: 'p7',
name: 'Denim Jeans',
description: 'Classic fit jeans',
category: 'Clothing',
brand: 'DenimCo',
price: 79.99,
rating: 4.3,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p8',
name: 'Running Shoes',
description: 'Lightweight running shoes',
category: 'Clothing',
brand: 'SportyFeet',
price: 129.99,
rating: 4.8,
inStock: true,
tags: ['new', 'popular'],
},
{
key: 'p9',
name: 'Winter Jacket',
description: 'Waterproof winter jacket',
category: 'Clothing',
brand: 'OutdoorGear',
price: 199.99,
rating: 4.5,
inStock: false,
tags: ['seasonal'],
},
{
key: 'p10',
name: 'Wool Sweater',
description: 'Merino wool sweater',
category: 'Clothing',
brand: 'EcoWear',
price: 89.99,
rating: 4.6,
inStock: true,
tags: ['eco-friendly'],
},
{
key: 'p11',
name: 'Coffee Maker',
description: 'Programmable coffee maker',
category: 'Home & Garden',
brand: 'BrewMaster',
price: 99.99,
rating: 4.4,
inStock: true,
tags: ['bestseller'],
},
{
key: 'p12',
name: 'Plant Pot Set',
description: 'Ceramic pot set of 3',
category: 'Home & Garden',
brand: 'GreenThumb',
price: 34.99,
rating: 4.1,
inStock: true,
tags: ['sale'],
},
{
key: 'p13',
name: 'LED Desk Lamp',
description: 'Adjustable LED lamp',
category: 'Home & Garden',
brand: 'LightWorks',
price: 59.99,
rating: 4.7,
inStock: true,
tags: ['featured'],
},
{
key: 'p14',
name: 'Air Purifier',
description: 'HEPA air purifier',
category: 'Home & Garden',
brand: 'CleanAir',
price: 249.99,
rating: 4.5,
inStock: true,
tags: ['new'],
},
{
key: 'p15',
name: 'Kitchen Scale',
description: 'Digital kitchen scale',
category: 'Home & Garden',
brand: 'CookPro',
price: 24.99,
rating: 4.2,
inStock: true,
tags: [],
},
],
{ model: 'Product' }
);
await dcupl.init();
// Create the product list
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Display catalog overview
console.log('=== Product Catalog ===\n');
console.log('Total products:', productList.catalog.query.count());
// Show facets
console.log('\n--- Categories ---');
productList.catalog.fn.facets({ attribute: 'category' }).forEach((f) => {
console.log(` ${f.value}: ${f.count} products`);
});
console.log('\n--- Brands ---');
productList.catalog.fn.facets({ attribute: 'brand' }).forEach((f) => {
console.log(` ${f.value}: ${f.count} products`);
});
// Apply filters
console.log('\n=== Applying Filters ===');
console.log('Category: Electronics');
console.log('Price: Under $200');
console.log('In Stock: Yes');
productList.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: 'Electronics',
});
productList.catalog.query.addCondition({ attribute: 'price', operator: 'lt', value: 200 });
productList.catalog.query.addCondition({ attribute: 'inStock', operator: 'eq', value: true });
// Display filtered results
const results = productList.catalog.query.items({
sort: { attributes: ['price'], order: ['asc'] },
});
console.log(`\n--- Results (${results.length} products) ---`);
results.forEach((p) => {
console.log(`\n ${p.name}`);
console.log(` ${p.price} | ${p.rating} stars | ${p.brand}`);
});
// Show price stats
const stats = productList.catalog.fn.aggregate({ attribute: 'price' });
console.log(`\n--- Price Range ---`);
console.log(` ${stats.min.toFixed(2)} - ${stats.max.toFixed(2)}`);
}
main();What You Learned
In this tutorial, you learned how to:
- Define models with various property types including arrays
- Load a realistic product dataset
- Build faceted navigation with dynamic counts
- Apply multiple filter conditions (AND logic)
- Filter by multiple values using the
inoperator - Filter by array properties (tags)
- Get price statistics and aggregations
- Sort results by single and multiple attributes
- Combine filters for complex queries
Key Patterns
Faceted Navigation
// Get counts for filter UI
const facets = list.catalog.fn.facets({ attribute: 'category' });
// Returns: [{ value: 'Electronics', count: 5 }, ...]Price Range Filter
list.catalog.query.addCondition({ attribute: 'price', operator: 'gte', value: 50 });
list.catalog.query.addCondition({ attribute: 'price', operator: 'lte', value: 200 });Multiple Values
list.catalog.query.addCondition({
attribute: 'brand',
operator: 'in',
value: ['Brand1', 'Brand2', 'Brand3'],
});Clear and Reapply
list.catalog.query.clear(); // Remove all filters
// Apply new filtersNext Steps
- Core Concepts: Models - Learn about property types, references, and validation
- Querying: Filtering - All filter operators
- Analysis: Facets - Advanced facet options
Common Issues
Facets show wrong counts
Facets count items based on the current query state. If you want global counts, call facets() before adding conditions, or clear the query first.
Multiple category filters not working
Use the in operator for multiple values:
// Wrong
query.addCondition({ attribute: 'cat', operator: 'eq', value: 'A' });
query.addCondition({ attribute: 'cat', operator: 'eq', value: 'B' }); // Overwrites!
// Correct
query.addCondition({ attribute: 'cat', operator: 'in', value: ['A', 'B'] });Array property filtering
When filtering array properties like tags, the eq operator checks if the array contains the value:
// Product has tags: ['new', 'featured']
query.addCondition({ attribute: 'tags', operator: 'eq', value: 'new' }); // Matches!