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

Step 1: Set Up the Project

Create a new project directory and install dependencies:

terminal
mkdir product-catalog
cd product-catalog
npm init -y
npm install @dcupl/core

Create the main application file:

catalog.ts
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:

catalog.ts
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:

catalog.ts
// ... 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:

catalog.ts
// ... 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.

catalog.ts
// 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:

terminal
npx tsx catalog.ts

Step 6: Filter by Category

Now simulate a user selecting "Electronics":

catalog.ts
// 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:

catalog.ts
// 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:

catalog.ts
// 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:

catalog.ts
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:

catalog.ts
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:

catalog.ts
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:

catalog.ts
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:

catalog.ts
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:

  1. Define models with various property types including arrays
  2. Load a realistic product dataset
  3. Build faceted navigation with dynamic counts
  4. Apply multiple filter conditions (AND logic)
  5. Filter by multiple values using the in operator
  6. Filter by array properties (tags)
  7. Get price statistics and aggregations
  8. Sort results by single and multiple attributes
  9. 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 filters

Next Steps

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!